Great question! This is a common issue when working with forms in the frontend, especially with libraries like Zod that distinguish between '', null, and undefined.
Your approach using z.preprocess is a solid and idiomatic way to handle this with Zod. It ensures that empty strings from your form are converted to null before validation, which is often what you want for optional or nullable fields.
Here’s a quick recap and a couple of alternative approaches you might find useful:
1. Your Approach (Recommended)
const schema = z.preprocess(
val => val === '' ? null : val,
z.string().nullable()
).optional();
This will treat empty strings as null, and the field is both nullable and optional.
2. Using .transform (If You Want to Always Get a String or Null)
If you want to ensure the output is always a string or null, you can use .transform:
const schema = z.string()
.transform(val => val === '' ? null : val)
.nullable()
.optional();
But note: .transform runs after validation, so if the value is '' and you don’t allow empty strings, validation will fail before transform runs. That’s why preprocess is usually better for this use case.
3. Custom Refinement (Not recommended for this use case)
You could use .refine, but it’s more verbose and less idiomatic for this scenario.
4. Handling at the Form Level
Alternatively, you could normalize your form data before passing it to Zod, converting all empty strings to null or undefined. This can be useful if you have many fields and want to apply the same logic everywhere.
function normalizeFormData(data) {
return Object.fromEntries(
Object.entries(data).map(([key, value]) => [key, value === '' ? null : value])
);
}
Summary
- Your solution using
z.preprocessis the most idiomatic and flexible for Zod. - If you have many fields, consider normalizing the data before validation.
- Avoid using
.transformfor this, unless you’re sure about the validation order.
Let me know if you have more questions or want to see how to apply this to an entire form schema!