zod icon indicating copy to clipboard operation
zod copied to clipboard

z.effect for preprocessing returns correct type but z.preprocess returns unknown for z.input

Open SaizFerri opened this issue 1 year ago • 5 comments

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>;

SaizFerri avatar Nov 07 '23 12:11 SaizFerri

This was already discussed here.

santosmarco avatar Nov 15 '23 02:11 santosmarco

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.

SaizFerri avatar Nov 17 '23 10:11 SaizFerri

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))

santosmarco avatar Nov 17 '23 11:11 santosmarco

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!

SaizFerri avatar Nov 17 '23 11:11 SaizFerri

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"] }

qraynaud avatar Apr 04 '24 10:04 qraynaud