Published on

Zod: Set Min and Max After Transforming String to Number

Authors
  • Name
    Ripal & Zalak
    Twitter

Zod: Min and Max Validation After Transforming String to Number

You have a number or numeric string, and you want to:

  1. Transform the numeric string into a number.
  2. Apply min and max constraints on the resulting number.

Using a straightforward approach like chaining .min() on a union schema doesn’t work because the methods are type-specific. Here’s a naive example:

const numberValid = z.number().or(z.string().regex(/\d+/).transform(Number))
const positiveNumber = numberValid.min(0) // Error: min() method doesn't exist

Solutions

1. Using z.coerce.number() (Version 3.20+)

In newer versions of Zod, you can use z.coerce.number() to achieve the desired behavior. This method ensures the input is coerced into a number, allowing you to apply .min() and .max() directly:

const numberValid = z.coerce.number().min(0)

console.log(numberValid.parse(123)) // OK
console.log(numberValid.parse('123')) // OK
console.log(numberValid.parse(-123)) // Error
console.log(numberValid.parse('-123')) // Error

2. Using refine for Custom Validation

If you’re using an older version of Zod or need a different approach, you can use .refine() to validate the range after transforming the value:

const numberValid = z
  .number()
  .or(z.string().regex(/\d+/).transform(Number))
  .refine((n) => n >= 0, { message: 'Value must be >= 0' })

console.log(numberValid.parse('123')) // OK
console.log(numberValid.parse('-123')) // Error: Value must be >= 0

3. Using z.preprocess for Pre-Validation Transformation

The z.preprocess method lets you transform input data before applying a schema. This allows you to preprocess strings into numbers and then apply min constraints:

const numberValid = z.preprocess((input) => {
  if (typeof input === 'string' && /^\d+$/.test(input)) {
    return Number(input)
  }
  return input
}, z.number().min(0))

console.log(numberValid.parse('123')) // OK
console.log(numberValid.parse(-123)) // Error: Number must be >= 0
console.log(numberValid.parse('abc')) // Error: Invalid input

4. Using pipe for Chaining Transformations

You can use .pipe() to chain transformations and validations sequentially:

const numberValid = z
  .string()
  .transform((val) => parseInt(val))
  .pipe(z.number().min(0))

console.log(numberValid.parse('123')) // OK
console.log(numberValid.parse('-123')) // Error

Key Considerations

  • Validation Order: Methods like z.preprocess ensure transformations occur before validation.
  • Error Messages: Customize error messages with the message property to provide better feedback.
  • Version Compatibility: Some features, like z.coerce.number(), are only available in newer Zod versions.

FAQs

Q1: What if my input is not guaranteed to be a numeric string?

Use a union schema with conditional transformations, or preprocess the input to handle edge cases.

Q2: How do I handle custom error messages?

You can pass an object with a message property to methods like .refine() or .min():

z.number().min(0, { message: 'Value must be zero or greater' })

Q3: Can I use these techniques in React or Node.js projects?

Yes, Zod is compatible with both environments and works seamlessly for validation in APIs, forms, and more.