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
.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.
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.
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');