zod
zod copied to clipboard
z.effect for preprocessing returns correct type but z.preprocess returns unknown for z.input
Hi,
first, thanks for this awesome package!
I found out by accident that z.preprocess()
would return unknown
if infering with z.input but, if using z.effect()
for preprocessing, z.input would return the right type. 2 questions:
- Why can't preprocess correctly infer the input type? I know that the arg is typed as
unknown
but what if we know that it will be a string? There is no way to pass a generic type to it. - Why does z.effect work and why I don't find it in the documentation? I think is worth to add this method of preprocessing.
const emptyStringToUndefined = (v) => (v === '' ? undefined : v);
const SchemaUnknown = z.object({
name: z.preprocess(emptyStringToUndefined, z.string().optional()),
});
const SchemaCorrect = z.object({
name: z.effect(z.string().optional(), {
type: 'preprocess',
transform: emptyStringToUndefined,
}),
});
// name?: unknown;
type A = z.input<typeof SchemaUnknown>;
// name?: string;
type B = z.input<typeof SchemaCorrect>;
This was already discussed here.
Thanks @santosmarco.
I agree with your point, there should be some kind of scape hatch if we really know the type, for example in form components.
My second point remains unanswered though. Why is z.effect
not documented and why does it work?
My usecase is a pretty simple one.
const Schema = z.object({
name: z.string().min(3).optional(),
});
A form input will always return an empty string if the input is empty. In this case validation would fail, but I want to allow this. What I'm doing is, use preprocess to convert an empty string to undefined and pass that to zod. After validation is done, I use transform
to convert an undefined to null so I remove it in the API (as undefined values won't be serialized). If I use z.input
to infer the type for my form schema, name
would be unknown
. If I use z.infer
, name
would be string | null
which is also not what the form should return, but rather what the request payload gets.
I'm just not sure if I have a thinking mistake here or zod is just missing this kind of case.
I'm not sure if I fully understand your problem, but if you want to accept strings of either length 0 or length 3 or more, while excluding strings with lengths 1 or 2, couldn't you simply do the following:
z.literal("").or(z.string().min(3))
I see. However this is still kind of wrong. I don't want an empty string, if it is empty, is considered unset or to be removed. I need to send to my API either a valid string or null
. In this case empty wouldn't be valid.
Of course I could append a transform in there and say, if empty string, transform to null and then my z.input
would work but feels kind of off. I find it easier to understand if I can just append an .optional()
. But thanks for your help!
I think your use case would be better served with something like:
const s = z.object({
name: z.string().optional().transform((val) => val || undefined),
});
s.parse({ name: "" }) // { name: undefined }
s.parse({ name: undefined }) // { name: undefined }
s.parse({ name: "someName" }) // { name: "someName" }
s.parse({}) // {}
For most complex use cases, you can first validate your value and use a subsequent parse, using once again transform
to emulate preprocess
:
const arrayOfUrlsSchema = z.string().url().array().min(1);
const strOfUrlsSchema = z
.string()
.transform((val) => arrayOfUrlsSchema.parse(val.trim().split(/\s*,\s*/g)));
const myObjSchema = z.object({
nodes: arrayOfUrlsSchema.or(strOfUrlsSchema),
});
myObjSchema.parse({ nodes: "scheme://host1, scheme://host2" }) // { nodes: ["scheme://host", "scheme://host2"] }