Skip to content

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:

  1. Accepts missing/undefined values
  2. Treats empty strings as undefined
  3. Requires non-empty strings to meet length constraints
  4. Provides clear error messages

Here's the most robust solution:

js
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:

  1. Combines your desired constraint with an empty string literal
  2. Transforms empty strings to undefined
  3. Marks the field as optional

Alternative Direct Implementation

If you prefer an inline solution:

js
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

  1. Union type: union() accepts either valid strings (min length of 4) or empty strings
  2. Transform: Converts empty strings to undefined for consistent handling
  3. Optional: Allows the field to be omitted or set to undefined
  4. Error precedence: The min constraint appears first for clear validation messages

Testing Behavior

js
// 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:

js
// ❌ 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:

js
type SchemaType = z.infer<typeof SocialsSchema>;
/*
  Equivalent to:
  {
    myField?: string | undefined;
  }
*/

Handling Null Values

To include null as a valid value:

js
z.union([
  z.string().min(4, "Value must be at least 4 characters"),
  z.literal("")
])
.nullish()
.transform(val => val === "" ? null : val)

Best Practices Recap

  1. Order matters: Place constrained types first in unions
  2. Use transforms to normalize values to undefined
  3. Build reusable helpers for consistent schemas
  4. Provide explicit error messages for better UX
  5. 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.