How to transform empty strings into null? `z.emptyStringToNull()`
How to support empty strings for z.string().datetime({ offset: true }).nullish() schema
if the string value is empty, I want it converted to null or keep the original value ie., ""
the following code throw error
import { afterAll, beforeAll } from 'vitest';
import { z } from 'zod';
describe('Test zod validations', () => {
it('should correctly handles a valid ISO date-string', () => {
const valid_from = '2022-12-14T22:07:10.430805+00:00';
const valid_to = undefined; <-- this works
const valid_to = null; <-- this works
const valid_to =""; <-- this is not working
const schema = z.string().datetime({ offset: true }).nullish();
expect(schema.parse(valid_from)).toStrictEqual(valid_from);
expect(schema.parse(valid_to)).toStrictEqual(valid_to);
});
});
ERROR
ZodError: [
{
"code": "invalid_string",
"validation": "datetime",
"message": "Invalid datetime",
"path": []
}
]
Serialized Error: {
"addIssue": "Function<>",
"addIssues": "Function<>",
"errors": [
{
"code": "invalid_string",
"message": "Invalid datetime",
"path": [],
"validation": "datetime",
},
],
"flatten": "Function<flatten>",
"formErrors": {
"fieldErrors": {},
"formErrors": [
"Invalid datetime",
],
},
"format": "Function<format>",
"isEmpty": false,
"issues": [
{
"code": "invalid_string",
"message": "Invalid datetime",
"path": [],
"validation": "datetime",
},
],
}
this worked for me
const schema = z.string().datetime({ offset: true }).nullish().or(z.string().max(0));
thanks @denvaki but I want const valid_to ="" treated same like valid_to = null;
ie., valid_to= ""; schema.parse(valid_to) == null
Reason:
I get FormData on the server side, form input field of type datetime-local is sent as empty string when this field is bind to null value in the browser. this is the default behavior for FormData.
when saving to DB, I like Zod transforming it automatically to null as I will be sending null to DB via update SQL.
Yeah, I have the exact same problem, problably zod can add a option to convert '' to null
You can solve with that:
export const dbOffsetDate = z.preprocess(
(arg) => (arg === '' ? null : arg),
z.string().datetime({ offset: true }).nullish(),
);
Then reuse the dbOffsetDate on your app
Wish we have built-in z.nuller like z.coerce
z.nuller.string().datetime({ offset: true }).nullish()
My reusable solution:
export function emptyToNull(arg: unknown) {
if (typeof arg !== 'string') {
return arg;
}
if (arg.trim() === '') {
return null;
}
return arg;
}
const valid_to = '';
const emptySchema = z.preprocess(emptyToNull, z.string().datetime({ offset: true }).nullish());
expect(emptySchema.parse(valid_to)).toStrictEqual(null);
another workaround:
const emptySchema = z.string().datetime({ offset: true }).nullish().catch(null);
I think the workarounds are sufficient here. I don't think any of the other string validators would accept an empty string as a valid option.
You wouldn't consider an empty string as a valid date
A z.nuller would convert empty strings to null (if nullable), or to undefined (if not nullable, but optional), and throw Required if not nullable or optional
Or return default if empty string or null
Now zod does not return default if the value is null and this behaviour is probably intentional, but in a new mode like z.nuller, it could treat null and empty strings as Required fields
z.string().datetime({ offset: true }).nullish() throwing invalid error, when string is empty seams like wrong, especially when I chain with .nullish()
I don’t like preprocess wrapper API.
Prefer either datatime({empty: null}) option or coerce style nuller
I think the workarounds are sufficient here. I don't think any of the other string validators would accept an empty string as a valid option.
You wouldn't consider an empty string as a valid date
I completely agree with @samchungy.
'' != null therefore I don't think this should be included in Zod.
Try using .transform to achieve your desired outcome.
Does this work for you?
const schema = z.string().nullish()
.transform( value => value === '' ? null : value )
.pipe( z.string().datetime().nullish() )
console.log( schema.parse( undefined ) ) // undefined
console.log( schema.parse( null ) ) // null
console.log( schema.parse( '' ) ) // null
console.log( schema.parse( '0000-00-00T00:00:00Z' ) ) // 0000-00-00T00:00:00Z
console.log( schema.safeParse( '000-00-00T00:00:00Z' ).success ) // false
console.log( schema.safeParse( 'invalid string' ).success ) // false
console.log( schema.safeParse( 42 ).success ) // false
console.log( schema.safeParse( false ).success ) // false
Solutions with transform and preprocess make code less appealing and boilerplate code ( two times nullish() & string() etc.)
I would like something like this:
z.coerce.string({allowEmpty: false}).nullish().parse(''); // => null
z.coerce.string({allowEmpty: false}).datetime().nullish().parse(''); // => null
z.coerce.string({allowEmpty: false}).datetime().nullish().parse(' '); // => null
z.coerce.string({allowEmpty: false}).datetime().nullish().parse("2020-01-01T00:00:00Z"); // pass
Solutions with
transformandpreprocessmake code less appealing and boilerplate code ( two times nullish() & string() etc.)I would like something like this:
z.coerce.string({allowEmpty: false}).nullish().parse(''); // => null z.coerce.string({allowEmpty: false}).datetime().nullish().parse(''); // => null z.coerce.string({allowEmpty: false}).datetime().nullish().parse(' '); // => null z.coerce.string({allowEmpty: false}).datetime().nullish().parse("2020-01-01T00:00:00Z"); // pass
What you're arguing for here isn't realistically possible and doesn't make much sense from a design pov.
What you want is for something to both convert an empty string to a null value as well as pass validation. That is
- a transformation of empty string to null
- Make the validator pass against something which isn't valid.
Think about it without transformations. What you are looking for is for something to match an empty string OR a valid date. An empty string !== a valid date.
The string coercer tries to convert an unknown into a string so that doesn't make sense.
To me preprocess here makes the most sense as you are looking to transform your input before validating.
Or matching against a datetime string union with an empty string and a transform to null for empty strings
If you opened up another issue about wanting an empty string to null coercer that would make more sense
Thanks for clarifying.
If you opened up another issue about wanting an empty string to null coercer that would make more sense
In my use case, I was transforming String data to Array/Set/Map and I have to check value is empty before converting them.
When dealing with the HTML FormData object, the values are always strings and if the user doesn't fill them, they come as empty strings to the server side.
on the server side, I wanted to convert them to null as that is what DB/GraphQL APIs expected for nullable columns.
Since I see this common pattern needed for handling FormData , I wanted to have Empty String to null built-in to zod.
https://github.com/xmlking/svelte-starter-kit/blob/main/src/lib/utils/zod.utils.ts#L66
export function ifNonEmptyString(fn: (value: string) => unknown): (value: unknown) => unknown {
return (value: unknown) => {
if (typeof value !== 'string') {
return value;
}
if (value === '') {
// return undefined;
return null;
}
return fn(value);
};
}
export const stringToArray = ifNonEmptyString((arg) => arg.split(','));
export const stringToMap = ifNonEmptyString((arg) => new Map(Object.entries(JSON.parse(arg))));
is this option make sense?
z.coerce.string().datetime({emptyToNull: true}).nullish().parse(' '); // => null
I don't think you follow.
An empty string !== datetime so you would never have any option in datetime like that.
Realistically if you wanted something it'd look more like:
z.emptyStringToNull().datetime().nullish()
Obviously emptyStringToNull would be named better
Agree z.emptyStringToNull() is what I am looking for. It will be useful for many of my use-cases
With the likes of Remix (and now Next.js) using forms, when submitting inputs which the user has left empty, the sent payload is always an empty string. So all my database columns are being updated to empty strings. I guess that's not so bad. I do manually transform them to null if it's a nullable field and the value is empty but something built in would be handy!
I'm now currently doing something like this:
export const NullableFormString = z.preprocess((v) => v === "" ? null: v, z.string().nullish())
Understand that this is very web/html/form specific, and for general data schemas it probably doesn't make sense, but I do feel a lot of users use Zod to validate and parse web form submissions and this is quite common.
just to add more options
function neutralValuesToNull<T extends z.ZodTypeAny>(zodType: T) {
return z.preprocess(
(val) => (val === "" || val === undefined ? null : val),
zodType
);
}
export const ZodSchema = z.object({
id: z.coerce.string().cuid(),
desc: neutralValuesToNull(z.coerce.string().max(1000).nullable()),
date: neutralValuesToNull(z.coerce.date().nullable()),
approved: neutralValuesToNull(
z.coerce.number().int().min(0).nullable()
),
})
I had the same problem with using zod with tRPC and i came up with an idea to parse all the rawInputs in a middleware so when they get to zod they are already transformed into undefined here is how i did it
const middleware = t.middleware(opts => {
if (opts.rawInput != null && typeof opts.rawInput == 'object' && !Array.isArray(opts.rawInput)) {
const inputs: Record<any, any> = opts.rawInput
Object.keys(inputs).forEach(key => {
if (inputs[key] == null || (typeof inputs[key] == 'string' && inputs[key] == '')) inputs[key] = undefined
if (Array.isArray(inputs[key]))
inputs[key] = (inputs[key] as any[]).filter(i => (i == null || (typeof i == 'string' && i == '') ? false : true))
})
opts.rawInput = inputs
}
return opts.next(opts)
})
So what is better?
export function z_coerciveOptional_PreprocessVersion<T extends z.ZodTypeAny>(t: T) {
return z.preprocess(
(val) => {
const isEmpty = typeof val == "number" && isNaN(val)
|| typeof val == "string" && val === ""
|| val === null
|| Array.isArray(val) && val.length == 0
return isEmpty ? undefined : val
}, t.optional()
)
}
export function z_coerciveOptional_TransformVersion<Z extends z.ZodTypeAny>(z: Z) {
return z.optional().transform(
(val: unknown) => {
const isEmpty = typeof val == "number" && isNaN(val)
|| typeof val == "string" && val === ""
|| val === null
|| Array.isArray(val) && val.length == 0
return isEmpty ? undefined : val
})
}
Both work identically AFAICT 🤔
There are a lot of ways to solve this, as others have stated. My preferred solution is probably something like this:
z.union([z.literal("").transform(() => null), z.string().datetime()]).nullish();
But ultimately @xmlking is right in that this is way too sophisticated for such a common use case. Form validation is one of the top use cases for Zod, and it needs to be addressed. Though z.emptyStringToNull() isn't a viable API, it's just way too specific.
The best idea I have currently is to add a key to .optional() and .nullable(). You can think of ZodOptional like this: if the input is undefined, then immediately stop parsing and return undefined. The new key would let you expand the set of "trigger values". (
z.string().datetime().optional({
inputs: [""] // if the input matches anything in this list, the schema returns `undefined`
})
This could optionally be a positional argument (but I think the behavior of this is pretty unclear).
z.string().datetime().optional([ "" ])
The same proposal could apply to .nullable()
Another idea is to add a method .empty() to ZodString which returns a ZodEffects instance.
z.string().empty(null); // => ZodEffects
// identical to
z.string().transform(val => val === "" ? null : val)
Another idea is to add a method .empty() to ZodString which returns a ZodEffects instance.
z.string().empty(null); // => ZodEffects
I low-key love this. So elegant. But the behaviour might be mistaken with a similar api: z.string().nonempty(). Though that API is deprecated so you could possibly remove it and introduce this in 4.0.0? What does z.string().empty() default to? undefined? Does it look more like this schema takes in only an empty string?
Would something like onEmpty() be more obvious that it's a transformation?
z.string().onEmpty(null);
onEmpty(null) would be a bit restrictive if you wanted to transform to something other than a primitive.
e.g. if you wanted empty strings to be transformed to a new instance of an empty array, it would need to accept a callback:
onEmpty(() => [])
... but at that point, you'd pretty much be back to transform((val) => val === "" ? [] : val) again 😅
We could accept both kinds of inputs?
What if you wanted the default to be a callback, though? 🥲
Think it'd be best to simply accept "a factory for a value" rather than "a factory for a value, or the value itself".
I'm here because I ran into the same issue (using Zod to validate a date input), it's clearly a pain point that a lot of us feel. And it's definitely widespread enough of a use case that it I think Zod should consider offering an ergonomic way of handling it.
I'm currently using this solution:
export const date = z.preprocess(
(arg) => (arg === '' ? null : arg),
z.string().date().nullish(),
)
But it's suboptimal because it gives me an unknown to deal with, the type in general is a mess:
const date: z.ZodEffects<z.ZodOptional<z.ZodNullable<z.ZodString>>, string | null | undefined, unknown>
I just want a simple way to use Zod to validate input from a date input. This simple task is surprising convoluted and not straightforward. There's clearly value in Zod supporting this use case in some native/ergonomic way.
Would something like onEmpty() be more obvious that it's a transformation?
Maybe whenEmptyUse() is more clear?