DEV Community

Cover image for Why Your Zod Validation Fails Before It Even Runs (And How to Fix It)
mohammad rostami
mohammad rostami

Posted on • Originally published at linkedin.com

Why Your Zod Validation Fails Before It Even Runs (And How to Fix It)

If you're using Zod with react-hook-form, you've probably seen this at least once:

Invalid input: expected number, received NaN

At first glance, it looks like a simple validation issue.
It’s not.

The real problem

When working with form inputs:

  • All values come in as strings
  • z.coerce.number() tries to convert them
  • Empty input ("") or invalid values → NaN

And here’s the catch:
Zod fails before your .min() / .max() validations run.

So instead of your custom message, you get a generic (and not very helpful) error.

It gets trickier

If you're using TypeScript:

  • z.input<typeof schema>z.output<typeof schema>
  • react-hook-form works with the input type
  • Zod gives you the output type after parsing

This mismatch can lead to confusing type errors and wrong assumptions about your data.

The fix

You need to handle invalid input before Zod tries to validate it:

readTime: z.preprocess((val) => {
  if (val === "" || val === undefined) return undefined;

  const num = Number(val);
  return isNaN(num) ? undefined : num;
},
z.number()
  .min(2, 'Minimum read time is 2 minutes')
  .max(60, 'Maximum read time is 60 minutes')
)
Enter fullscreen mode Exit fullscreen mode

What this solves

  • Empty input → handled properly
  • Invalid numbers → no more NaN issues
  • Custom validation messages → actually shown
  • Cleaner UX overall

Takeaway

If you're using z.coerce.number() in forms, don't rely on it blindly.
Always normalize your input first.

Because sometimes the bug isn't in your validation rules…
it's in how your data arrives before validation even starts.

Top comments (0)