zod-dev
TypeScript icon, indicating that this package has built-in type declarations

1.2.5 • Public • Published

🦞 Zod Dev

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 ⭐

Motivation

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.

Basic Usage

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: 'john@smith.com',
    age: 24,
}

const result = schema.devParse(value)

image

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.

Constructors

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: 'john@smith.com',
    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: 'john@smith.com',
    age: 24,
}

const result = devParse(value, schema)

CRUD Operations

Due to the ubiquity of immutable helper functions as a means to manipulate data in Data-oriente Programming, zod-dev also provides the means to create a basic CRUD helper functions as a means to manipulate objects that share the same schema in an array list.

This is done by making use of createArrayOperations which accepts a schema and returns an object as follows:

All helpers automatically run devParse on all inputs and outputs to ensure data integrity.

import { z } from "zod";
import { withDev, createArrayOperations } from "zod-dev";
const isDev = import.meta.env.MODE !== "production";

const schema = z.object({
    name: z.string(),
    email: z.string().email(),
    age: z.number().int().min(0),
})

export const { isCollection, getIndices, get, add, update, remove } = createArrayOperations(
  isDev,
  schema
);

let data = isCollection([
    {
        id: '4a932261-93dc-448a-91e5-bf3541510cfa',
        name: 'John Smith',
        email: 'john@smith.com',
        age: 24,
    },
    {
        id: '61627dac-2cb5-4934-a9bb-c2075e932ce8',
        name: 'Lorem Ipsum ',
        email: 'lorem@ipsum.com',
        age: 39,
    }
])

console.log(getIndices(data, '61627dac-2cb5-4934-a9bb-c2075e932ce8')) // [1]
console.log(getIndices(data, (item) => item.email.includes('@'))) // [0, 1]

// `get` works exactly like `getIndices` but returns the actual items instead
console.log(get(data, ['4a932261-93dc-448a-91e5-bf3541510cfa']))
console.log(get(data, (item) => item.email.includes('@')))

data = remove(data, '61627dac-2cb5-4934-a9bb-c2075e932ce8')
data = remove(data, (item) => item.name === 'John Smith')

// data is now `[]`

data = add(
    data, 
    {
        id: '61627dac-2cb5-4934-a9bb-c2075e932ce8',
        name: 'Lorem Ipsum ',
        email: 'lorem@ipsum.com',
        age: 39,
    }
)

data = add(
    data, 
    {
        id: '4a932261-93dc-448a-91e5-bf3541510cfa',
        name: 'John Smith',
        email: 'john@smith.com',
        age: 24,
    },
    'start'
)

// data is now same when created

data = update(data, (item) => ({ ...item, age: item.age + 1 })) 
// Ages are now `25` and `40`

data = update(data, (item) => ({ ...item, name: item.name.toReversed() }), '4a932261-93dc-448a-91e5-bf3541510cfa')
// `John Smith` name is now `htimS nhoJ`

Performance

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:

image

If you're interested in the reason for the difference you can have a look at the follow conversation.

FAQ

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.

Package Sidebar

Install

npm i zod-dev

Weekly Downloads

21

Version

1.2.5

License

MIT

Unpacked Size

1.13 MB

Total Files

12

Last publish

Collaborators

  • schalkventer