zod
zod copied to clipboard
Support coercion for arrays
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
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 ",".
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.
@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
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
// }
This feels very specific to me, and doesn't strike me as general-purpose API that would be widely used. Solving this with
.preprocessis 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.
This feels very specific to me, and doesn't strike me as general-purpose API that would be widely used. Solving this with
.preprocessis exactly what I'd recommend here.
It is not clear to me how to enable array validation processing after preprocessing a string like this.
export const arrayFromString = <T extends ZodTypeAny>(schema: T) => {
return z.preprocess((obj: unknown) => {
if (!obj) {
return []
}
if (Array.isArray(obj)) {
return obj
}
if (typeof obj === 'string') {
return obj.split(',')
}
return []
}, z.array(schema))
}
// Expected array, received string
roleIds: arrayFromString(z.string().min(1, 'required')).array()
.min(1, 'min 1 items'),
roleIds = "1,2,3"
roleIds = ""
@mschipperheyn perhaps like this:
export const arrayFromString = <T extends ZodTypeAny>(schema: T) => {
return z.preprocess((obj: unknown) => {
if (!obj) {
return [];
}
if (Array.isArray(obj)) {
return obj;
}
if (typeof obj === 'string') {
return obj.split(',');
}
return [];
}, z.array(schema));
};
const foo = arrayFromString(
z.union([z.literal('FOO'), z.literal('BAR')])
).refine((value) => {
return value.length > 0;
});
const correct = foo.parse('FOO,BAR');
const incorrect = foo.parse('FOO,BAR,BAZ');