-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
How to transform empty strings into null? z.emptyStringToNull()
#1721
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
this worked for me const schema = z.string().datetime({ offset: true }).nullish().or(z.string().max(0)); |
thanks @denvaki but I want ie., Reason: when saving to DB, I like Zod transforming it automatically to |
Yeah, I have the exact same problem, problably You can solve with that: export const dbOffsetDate = z.preprocess(
(arg) => (arg === '' ? null : arg),
z.string().datetime({ offset: true }).nullish(),
); Then reuse the |
Wish we have built-in
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 Or return Now zod does not return |
|
I completely agree with @samchungy. Try using |
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 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
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.
In my use case, I was transforming String data to Since I see this common pattern
8000
needed for handling FormData , I wanted to have 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:
Obviously |
Agree |
z.emptyStringToNull()
z.emptyStringToNull()
z.emptyStringToNull()
?
z.emptyStringToNull()
?z.emptyStringToNull()
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 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 🤔 Post UpdateFound one difference concerning Transform basedlet z_optionalString1 = z.string().min(2).max(10).optional().transform(s =>
s === undefined || s === null || s === "" ? undefined : s
)
Preprocess basedlet z_optionalString2 = z.preprocess((v) => v === "" ? undefined: v, z.string().min(2).max(10).optional())
Union + Transform based (proposed by @colinhacks below)function optionalString(str: z.ZodString) {
return z.union([z.literal("").transform(() => undefined), str.optional()])
}
let z_optionalString3 = optionalString(z.string().min(2).max(10))
|
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 The best idea I have currently is to add a key to 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 Another idea is to add a method z.string().empty(null); // => ZodEffects
// identical to
z.string().transform(val => val === "" ? null : val) |
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 z.string().onEmpty(null); |
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:
... but at that point, you'd pretty much be back to |
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 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.
Maybe |
Just encountered this issue on a project. Look forward to it getting resolved |
Thank you for this! |
This would be incompatible with
So |
Also would like to state my support for this. I'm dealing with an API that has a lot of questionable design decisions, one being returning an empty string instead of null or some discriminated union to indicate no value. Having a simple "if empty then use null" feature in Zod would be great for this. |
I'm in the same case as @lloydjatkinson. I'm using an API that has a lot of questionable design decisions, so I needed some way to handle these empty strings globally. Here is how I'm using it: z.deserializers.add(ZodString, function (input) {
if (typeof input.data === "string") {
const isBlank = input.data.trim().length === 0;
input.data = isBlank ? undefined : input.data;
}
return input;
}); And here is the implementation: import {
ParseInput,
z,
ZodArray,
ZodBigInt,
ZodBoolean,
ZodDate,
ZodNumber,
ZodObject,
ZodString,
ZodSymbol,
ZodType,
ZodTypeDef,
} from "zod";
declare module "zod" {
export namespace z {
const deserializers: Deserializers;
}
}
type Deserializer<
Output = unknown,
Def extends ZodTypeDef = ZodTypeDef,
Input = Output
> = (this: ZodType<Output, Def, Input>, input: z.ParseInput) => z.ParseInput;
// Typesafe Heterogenous Containers Pattern
class Deserializers {
private readonly deserializers = new Map<
object,
Array<(...args: Array<never>) => unknown>
>();
public add<
Output = unknown,
Def extends ZodTypeDef = ZodTypeDef,
Input = Output
>(
type: typeof ZodType<Output, Def, Input>,
deserializer: Deserializer<Output, Def, Input>
): void {
this.deserializers.set(type, [...this.get(type), deserializer]);
}
public get<
Output = unknown,
Def extends ZodTypeDef = ZodTypeDef,
Input = Output
>(
type: typeof ZodType<Output, Def, Input>
): Array<Deserializer<Output, Def, Input>> {
return (this.deserializers.get(type) ?? []) as Array<
Deserializer<Output, Def, Input>
>;
}
}
Reflect.set(z, "deserializers", new Deserializers());
function augment<
Output = unknown,
Def extends ZodTypeDef = ZodTypeDef,
Input = Output
>(type: typeof ZodType<Output, Def, Input>) {
type.prototype._parse = new Proxy(type.prototype._parse, {
apply(
target,
thisArg: ZodType<Output, Def, Input>,
argArray: [ParseInput]
) {
const [input] = argArray;
const deserializers = z.deserializers.get(type);
const deserializedInput = deserializers.reduce(
(accumulator, deserializer) => deserializer.call(thisArg, accumulator),
input
);
return target.call(thisArg, deserializedInput);
},
});
}
augment(ZodObject);
augment(ZodArray);
augment(ZodString);
augment(ZodNumber);
augment(ZodBigInt);
augment(ZodBoolean);
augment(ZodDate);
augment(ZodSymbol); |
Now that zod 4 beta is out, will there be some "blessed" path for this? Though I do like |
How to support empty strings for
z.string().datetime({ offset: true }).nullish()
schemaif the string value is empty, I want it converted to
null
or keep the original value ie.,""
the following code throw error
ERROR
The text was updated successfully, but these errors were encountered: