Conditionally disables Zod run-time parsing in production while preserving type inference.
⭐ If you find this tool useful please consider giving it a star on Github ⭐
Primarily inspired by Yehonathan Sharvit's use of conditional validation with AJV as part of a data‑oriented programming approach.
Moreover, in Data-Oriented Programming, it is quite common to have some data validation parts enabled only during development and to disable them when the system runs in production.
— Data-oriented Programming (2022)
There are several benefits to using Zod over AJV, the most prominent being the automatic inference of static types from schemas.
However, Zod is primarily designed for strict type safety,
especially for use with TypeScript at the edges of your project's data ingress
and egress. For this reason, Zod does not naturally lend
itself well to pure JavaScript projects, usage of JSDoc and/or more loosely
typed TypeScript projects by means of // @ts-check
.
npm install zod zod-dev
Simply wrap your Zod schema with the withDev
functional
mixin and provide a condition that determines whether run-time parsing should be
enabled. For example, if you are using Vite, the
following should suffice:
import { z } from 'zod';
import { withDev } from 'zod-dev'
const isDev = import.meta.env.MODE !== "production"
const schema = withDev(isDev, z.object({
name: z.string(),
email: z.string().email(),
age: z.number().int().min(0),
}))
const value = {
name: 'John Smith',
email: '[email protected]',
age: 24,
}
const result = schema.devParse(value)
Note that withDev
leaves the original schema untouched, so you can still, for
example, use person.parse(value)
or person.shape.email
should you wish.
If you don't want to pass the condition manually each time you can use one of the following factory functions that implicitly include the condition. This means that you don't need to manually pass the condition each time you create a schema. Both serve the same purpose, and are simply a matter of preference.
The first, createWithDev
create a custom withDev
that automatically includes
the condition:
import { z } from 'zod';
import { createWithDev } from 'zod-dev'
const isDev = import.meta.env.MODE !== "production"
const withDev = createWithDev(isDev)
const schema = withDev(isDev, z.object({
name: z.string(),
email: z.string().email(),
age: z.number().int().min(0),
}))
const value = {
name: 'John Smith',
email: '[email protected]',
age: 24,
}
const result = schema.devParse(value)
The second, createDevParse
, creates devParse as a stand-alone function that
accepts any value and schema. This provides a bit more flexibility since it is
not bound to a specific schema:
import { z } from 'zod';
import { createDevParse } from 'zod-dev'
const isDev = import.meta.env.MODE !== "production"
const devParse = createDevParse(isDev)
const schema = z.object({
name: z.string(),
email: z.string().email(),
age: z.number().int().min(0),
})
const value = {
name: 'John Smith',
email: '[email protected]',
age: 24,
}
const result = devParse(value, schema)
Due to the nature of Zod's schema inference, it is several orders of magnitude slower than AJV for run-time parsing. This means that even when using Zod in a strict type-safety manner, there might still be performance benefits to disabling run-time validation in production environments.
As per Runtype Benchmarks:
If you're interested in the reason for the difference you can have a look at the follow conversation.
What value should I use to toggle run-time checking?
This plugin was created for the use case of toggling run-time checking between different environments. However, since it merely accepts a boolean condition, parsing can effectively be toggled based on anything that can be expressed as true or false in JavaScript.
Should I use conditional run-time checking everywhere?
No. It is recommended that you still use .parse
and/or .safeParse
as
intended when validating external consumed by you app. For example during form
submissions or JSON data from an REST endpoint.