Alex
Alex

Reputation: 67248

Coerce string to literal number with Zod

I have a property that is supposed to allow only certain numbers:

{
   ...
   x: 1 | -1
}

How to define this in the input validation schema?

If input is JSON it's easy:

x: z.union([z.literal(-1), z.literal(1)])

but if input comes in the search query, then values are strings, so I need to be able to coerce to number, but still limit to -1 and 1 to make the inferred type from the schema compatible with the TypeScript type.

Upvotes: 2

Views: 113

Answers (1)

remix23
remix23

Reputation: 3019

One way to do that would be to accept both numbers and strings, transform strings to number then refine the result to parse only allowed numbers:

// union of
z.union([
  z.number(), // number or
  z.string().transform(str => parseInt(str, 10)) // string transformed to number
]).refine(v => [1,-1].includes(v)); // refined to allow a set of numbers

or alternatively pipe the transformation to your original schema:

// union of
z.union([
  z.number(), // number or
  z.string().transform(str => parseInt(str, 10)) // string transformed to number
]).pipe(z.union([z.literal(1), z.literal(-1)]))

Or you can use a preprocess also followed by a pipe:

z.preprocess(
    v => typeof v === 'number' ? v : parseInt(v, 10), // preprocessing
    z.number() // as number
).pipe(z.union([z.literal(1), z.literal(-1)]));

The docs also point to coerce which can work the same way and matches more closely your original question albeit with less control on the conversion:

// coerce internally uses Number(value)
z.coerce.number().pipe(z.union([z.literal(1), z.literal(-1)]));

The resulting type should correctly be inferred as number in all cases and probably as the required union in the pipe cases. Just choose your favorite flavor and control level :)

zod playground

Upvotes: 1

Related Questions