Published on

Validate Numbers with Up to 2 Decimal Places Using Zod

Authors
  • Name
    Ripal & Zalak
    Twitter

Validating Numbers with Two Decimal Places Using Zod

In many TypeScript or JavaScript projects, you might need to ensure a number has up to two decimal places. For example:

  • 1, 1.1, 1.11
  • 1.111, 1.12345

Here’s how you can achieve this using the popular Zod validation library.


Problem

By default, Zod provides a powerful schema validation mechanism, but ensuring precision in decimal places can be tricky. A naive solution is to use z.number().multipleOf(0.01):

import { z } from 'zod'

const schema = z.number().multipleOf(0.01)
schema.parse(1.11) // ✅ Passes
schema.parse(1.111) // ❌ Throws error

This works in most cases but may fail due to floating-point precision issues. For instance:

schema.parse(0.1 + 0.1 + 0.1) // ❌ Fails

The reason? 0.1 + 0.1 + 0.1 becomes 0.30000000000000004 internally.


A Better Approach Using refine()

To reliably validate numbers with up to two decimal places, use Zod's refine() method. This method allows custom validation logic, such as handling floating-point quirks with Number.EPSILON:

import { z } from 'zod'

const twoDecimalSchema = z.number().refine(
  (value) => {
    const multiplier = 100
    const fractionalPart = value * multiplier - Math.trunc(value * multiplier)
    return Math.abs(fractionalPart) < Number.EPSILON
  },
  {
    message: 'Number must have up to two decimal places',
  }
)

// Usage
twoDecimalSchema.parse(1.11) // ✅ Passes
twoDecimalSchema.parse(1.111) // ❌ Throws error

This approach accounts for floating-point precision issues by comparing against Number.EPSILON.


Explanation

  1. Why Number.EPSILON?

    • Floating-point arithmetic in JavaScript can produce small errors. Number.EPSILON provides a threshold to check for approximate equality.
  2. How does it work?

    • Multiply the number by 100.
    • Subtract its truncated value (Math.trunc).
    • Validate that the result is within the small threshold of Number.EPSILON.
  3. Why refine() over custom()?

    • Both are valid options. However, refine() is concise, easier to use, and integrates seamlessly with error messages.

FAQs

Q: Can I validate decimal places for strings instead?

Yes! If your input is a string, you can use z.string().regex():

const stringSchema = z.string().regex(/^\d+(\.\d{1,2})?$/, {
  message: 'Must have up to 2 decimal places',
})

Q: What happens if the input has more than two decimal places?

The schema will throw a validation error with the custom message you defined.

Q: Why not use multipleOf(0.01)?

multipleOf(0.01) works but may fail for numbers generated by operations like 0.1 + 0.1 + 0.1. Use refine() for higher reliability.