zod icon indicating copy to clipboard operation
zod copied to clipboard

Support coercion for arrays

Open kibertoad opened this issue 1 year ago • 5 comments

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

kibertoad avatar Oct 17 '23 11:10 kibertoad

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 ",".

joaopbnogueira avatar Jan 22 '24 14:01 joaopbnogueira

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 avatar Apr 22 '24 00:04 colinhacks

@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

kibertoad avatar Apr 22 '24 07:04 kibertoad

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
// }

kalgon avatar May 29 '24 11:05 kalgon

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.

blujedis avatar Jun 17 '24 03:06 blujedis

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 avatar Jul 10 '24 13:07 mschipperheyn

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

image

airtonix avatar Sep 02 '24 05:09 airtonix