colinhacks / zod

TypeScript-first schema validation with static type inference
https://zod.dev
MIT License
31.74k stars 1.1k forks source link

Support coercion for arrays #2873

Open kibertoad opened 8 months ago

kibertoad commented 8 months ago

Currently there is no way to tell Zod "if you get a single value of correct type, just put it into an array". Existing .coerce() operation could be used for this.

Here is a reference implementation of this logic: https://github.com/lokalise/zod-extras/blob/main/src/utils/toArrayPreprocessor.ts Test: https://github.com/lokalise/zod-extras/blob/main/src/utils/toArrayPreprocessor.test.ts

joaopbnogueira commented 5 months ago

I would take this a step further and allow the "coerce" function to be specified instead of allowing only a default per type. E.g. for arrays, a "coerce" could split an input string by ",".

colinhacks commented 2 months ago

This feels very specific to me, and doesn't strike me as general-purpose API that would be widely used. Solving this with .preprocess is exactly what I'd recommend here.

kibertoad commented 2 months ago

@colinhacks Is it? For any webservice that relies on zod for defining request schemas this is a very common case for handling request query params, when array value arrives with a single entry, and hence web framework has no way to determining on its own whether it's a string or an array of strings

kalgon commented 1 month ago

I am using something like this in the meantime if anybody is interested (there is certainly room for improvement):

const asArray = <T extends ZodTypeAny>(type: T) => z.any().transform<z.infer<typeof type>[]>((value: any) => Array.isArray(value) ? value.map(el => type.parse(el)) : [type.parse(value)]);
const asSingle = <T extends ZodTypeAny>(type: T) => z.any().transform<z.infer<typeof type>>((value: any) => type.parse(Array.isArray(value) && value.length > 0 ? value[0] : value));

const zMyParams = z.object({
  foo: asArray(z.string()),
  bar: asArray(z.coerce.number()),
  baz: asSingle(z.string()),
  qux: asSingle(z.coerce.number())
});

type MyParams = z.output<typeof zMyParams>;

// gives:
// type MyParams = {
//   foo: string[];
//   bar: number[];
//   baz: string;
//   qux: number;
// }

console.log(zMyParams.parse({
  foo: ['one', 'two'],
  bar: ['1', '2'],
  baz: ['one', 'two'],
  qux: ['1', '2']
}));

// gives:
// {
//  "foo": ["one", "two"],
//  "bar": [1, 2],
//  "baz": "one",
//  "qux": 1
// }

console.log(zMyParams.parse({
  foo: 'one',
  bar: '1',
  baz: 'one',
  qux: '1'
}));

// gives:
// {
//   "foo": ["one"],
//   "bar": [1],
//   "baz": "one",
//   "qux": 1
// }
blujedis commented 2 weeks ago

This feels very specific to me, and doesn't strike me as general-purpose API that would be widely used. Solving this with .preprocess is exactly what I'd recommend here.

Tinkering with this earlier, I would agree, and it works as intended, however in some cases types resulted in "any" or "unknown" Which is problematic. An example here might be a good thing.