Zod Optional Field with Minimum Length Constraint
Problem Statement
When working with Zod schemas, developers often need to create a field that is either:
- Optional: Can be
undefined
, missing, or an empty string - Minimum length: When present, must be at least 4 characters long
The challenge arises because Zod's default .optional()
method doesn't treat empty strings as absent values. A field using z.string().min(4).optional()
will accept values like "valid"
(length=4) but reject empty strings ""
, throwing a validation error.
Solution Overview
The optimal solution requires combining multiple Zod methods to create a type-safe schema that:
- Accepts missing/undefined values
- Treats empty strings as undefined
- Requires non-empty strings to meet length constraints
- Provides clear error messages
Recommended Solution: Union with Transform
Here's the most robust solution:
import { z } from "zod";
function optionalWithEmpty<T extends z.ZodTypeAny>(schema: T) {
return z
.union([schema, z.literal("")])
.transform(value => value === "" ? undefined : value)
.optional();
}
export const SocialsSchema = z.object({
myField: optionalWithEmpty(
z.string().min(4, "Value must be at least 4 characters")
)
});
This solution uses a reusable optionalWithEmpty
helper that:
- Combines your desired constraint with an empty string literal
- Transforms empty strings to
undefined
- Marks the field as optional
Alternative Direct Implementation
If you prefer an inline solution:
export const SocialsSchema = z.object({
myField: z
.union([
z.string().min(4, "Value must be at least 4 characters"),
z.literal("")
])
.optional()
.transform(value => value === "" ? undefined : value)
});
Explanation
Why This Works
- Union type:
union()
accepts either valid strings (min length of 4) or empty strings - Transform: Converts empty strings to
undefined
for consistent handling - Optional: Allows the field to be omitted or set to
undefined
- Error precedence: The min constraint appears first for clear validation messages
Testing Behavior
// Success cases
SocialsSchema.parse({}) // Missing field → { myField: undefined }
SocialsSchema.parse({ myField: undefined }) // Explicit undefined
SocialsSchema.parse({ myField: "" }) // Empty string → { myField: undefined }
SocialsSchema.parse({ myField: "abcd" }) // Valid string
// Failure cases
SocialsSchema.parse({ myField: "abc" }) // Error: Too short (length=3)
SocialsSchema.parse({ myField: 123 }) // Error: Not a string
Common Pitfalls to Avoid
Anti-Patterns
Avoid these approaches that seem similar but don't work:
// ❌ Doesn't handle empty strings
z.string().min(4).optional()
// ❌ Allows empty strings but keeps them as ""
z.string().min(4).optional().or(z.literal(""))
// ❌ Allows null but not empty strings
z.string().min(4).nullish()
Advanced Implementation
Type Safety Considerations
The solution maintains proper TypeScript type inference:
type SchemaType = z.infer<typeof SocialsSchema>;
/*
Equivalent to:
{
myField?: string | undefined;
}
*/
Handling Null Values
To include null
as a valid value:
z.union([
z.string().min(4, "Value must be at least 4 characters"),
z.literal("")
])
.nullish()
.transform(val => val === "" ? null : val)
Best Practices Recap
- Order matters: Place constrained types first in unions
- Use transforms to normalize values to
undefined
- Build reusable helpers for consistent schemas
- Provide explicit error messages for better UX
- Test edge cases: empty strings, null, undefined, missing fields
Runtime Performance
This solution uses Zod's highly optimized parsing engine and adds minimal overhead. The transform step operates only on present values.
Conclusion
Implementing optional fields with minimum length constraints in Zod requires combining union()
, optional()
, and transform()
to handle edge cases properly. The reusable optionalWithEmpty
helper simplifies this pattern across your codebase while maintaining type safety and clear validation.
For complex scenarios, consider composing additional constraints with Zod's .refine()
method to maintain readable schemas.