Schema diagrams being compiled into fast beams of light, with a gray-blue cat watching from a shelf.

zod-compiler: Zod at the Speed of Light, Without Touching Your Code

The Gray Cat
The Gray Cat
1 view

If you write TypeScript, you have almost certainly written Zod. It is the friendliest way to describe the shape of your data, and the type inference is so good that the schema and the type feel like the same thing. The only catch is what happens at runtime. Every time you call .parse() or .safeParse(), Zod walks the schema tree node by node, re-deciding how to validate each field on every single call. For a form that runs once when the user clicks submit, nobody notices. For an API route that validates millions of request bodies, that tree-walking shows up in your flame graphs.

zod-compiler takes a different angle on the problem. Instead of asking you to rewrite your schemas into something faster, it adds a plugin to your bundler that finds your existing Zod schemas and compiles them into flat, inlined validation functions at build time. Your application code does not change at all. You still import z, you still write z.object({...}), and you still call .parse(). The plugin rewrites what those schemas do under the hood so that the runtime work is mostly gone before your code ever ships. Depending on how nested your data is, the result is anywhere from a little faster to dramatically faster, with the project claiming gains of up to 75x on deeply nested objects.

Why Build-Time Compilation Changes the Math

Zod v4 already does some clever work. It uses JIT compilation through new Function() to speed up hot paths. But the schema is still an object that gets interpreted, and there is real overhead in deciding, on every call, how to validate a given node. The insight behind zod-compiler is that the shape of your validation almost never changes between calls. A CreateUserSchema validates the same fields in the same order every time. So why redo that planning work at runtime when you can do it once at build time?

The plugin runs a four-step pipeline during your build. First it uses a quick regex pre-filter to skip any file that does not actually import Zod at runtime, ignoring type-only imports. Then it inspects the surviving files, loading them and detecting which exports are genuine Zod schemas. For each schema it walks the tree once and generates flat, inlined validation code. Finally it uses an AST transform to surgically swap the original schema definition for the compiled one, while keeping the real Zod object available through the prototype chain. That last detail is what makes the whole thing safe: tools like tRPC, Hono, and React Hook Form that inspect .shape or .keyof() keep working, because the metadata they read is still right there.

The heart of the speedup is what the project calls the fast path. For valid inputs, the compiled validator is a single boolean expression chain with zero allocations. There is no result object, no issues array, and the input is returned by reference. This is about as cheap as a validation check can get. If the fast path fails, only then does a second, fuller validator run to collect errors, and even that work is deferred until you actually read the .error property. Since the overwhelming majority of inputs in production are valid, your code spends almost all of its time on the cheapest possible path.

Features Worth Knowing About

  • Zero code changes in auto mode. Add the plugin, keep your schemas exactly as they are. The compiler discovers exported schemas automatically.
  • Broad bundler support. First-class plugins for Vite, webpack, esbuild, Rollup, Rolldown, rspack, plus Bun and Farm.
  • Partial compilation. If a single property uses something it cannot compile, only that property falls back to plain Zod. Everything else stays compiled.
  • Schema hoisting. Schemas defined inside functions are automatically lifted to module scope so they are not rebuilt on every call, which is a quiet win for React components and request handlers.
  • Bundle-size controls. A compact output mode and cross-file deduplication keep the generated code small.
  • CI diagnostics. A check command reports how much of your schema surface actually compiled, so you can gate it in CI.

Getting It Into Your Project

Installation is a single dependency.

npm install --save-dev zod-compiler

Or with yarn:

yarn add --dev zod-compiler

You also need Zod itself, which you almost certainly already have. The compiler is built around modern Zod (v4-era APIs like z.email()), so make sure your Zod is current.

Wiring Up the Bundler

Most teams use the auto mode, where you add the plugin and let it find your schemas. Here is a Vite setup:

import { defineConfig } from "vite";
import zodCompiler from "zod-compiler/vite";

export default defineConfig({
  plugins: [zodCompiler()],
});

The same shape works for the other bundlers, just swap the import path: zod-compiler/webpack, zod-compiler/esbuild, zod-compiler/rollup, zod-compiler/rolldown, or zod-compiler/rspack. Once the plugin is in place, your schemas are compiled as part of the normal build with no further configuration.

Your Schemas Stay Exactly the Same

This is the part that feels almost too good. You do not change a single line of your validation code. A schema you wrote a year ago compiles just as well as one you write today.

import { z } from "zod";

export const CreateUserSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.email(),
  age: z.number().int().min(0).max(150),
});

And your call sites are untouched too:

const user = CreateUserSchema.parse(data);

const result = CreateUserSchema.safeParse(data);
if (result.success) {
  // result.data is fully typed
}

if (CreateUserSchema.is(data)) {
  // data is narrowed to the schema's type
}

Behind the scenes, CreateUserSchema.parse is now backed by compiled code. From your application's point of view, nothing happened except that validation got faster.

What Actually Gets Compiled

A fair question is how much of your schema surface this can really handle. The answer is most of it. Strings, numbers, objects, arrays, unions, discriminated unions, enums, tuples, records, sets, maps, and dates all compile, along with the usual constraints like min, max, email, uuid, regex, and int. Even lazy and recursive schemas are supported.

A handful of things fall back to plain Zod by design: callbacks that capture external variables, superRefine, custom, preprocess, and lazy getters that cannot be resolved at build time. The important thing is that this fallback is granular. If one field in a large object uses superRefine, only that field reverts to the interpreted path while every other field stays compiled. You are never forced to choose between compiling the whole schema or none of it.

You can see exactly where you stand with the diagnostics command:

npx zod-compiler check src/schemas.ts

It reports your compile coverage, which schemas are eligible for the zero-allocation fast path, and hints for raising your numbers. In CI you can turn that into a gate:

npx zod-compiler check src/ --json --fail-under 80

This fails the build if less than 80 percent of your schemas compile, which is a nice way to catch a regression where someone introduces a pattern that quietly drops you back to the slow path.

Tuning the Output for Real Projects

Trimming the Bundle With Compact Mode

The fully compiled validators include their own error-reporting code, which is great for speed but adds bytes. If you have many distinct schemas and care about bundle size, compact mode keeps the fast path fully compiled but hands error reporting back to the original Zod schema.

import { defineConfig } from "vite";
import zodCompiler from "zod-compiler/vite";

export default defineConfig({
  plugins: [
    zodCompiler({
      output: "compact",
    }),
  ],
});

The project reports this can cut roughly 73 percent off the generated code for distinct schemas, because the error-collecting paths are no longer duplicated across every schema. On top of this, the plugin deduplicates shared runtime helpers into a single virtual module and shares error-collecting walks between structurally identical sub-trees across files, which further shrinks the output on realistic schema sets.

Matching Zod's Behavior Where It Differs

This is the section to read carefully, because the compiled validators intentionally differ from stock Zod in a few ways, and these differences can bite if you are not expecting them.

By default, compiled object validators retain unknown keys rather than stripping them. Zod's default is to strip keys that are not in the schema, so if your code relies on that stripping you need to ask for it:

zodCompiler({
  stripUnknownKeys: true,
});

Two other behaviors are worth internalizing. The compiled validator returns the input value by reference on success rather than constructing a fresh object, which is part of how it achieves zero allocations. And record validation iterates only enumerable string keys, skipping symbols and non-enumerable keys. For the vast majority of payloads this is exactly what you want, but if you depend on Zod handing you a brand-new object or on symbol keys being validated, plan accordingly.

There is one operational caveat in auto mode as well. Because schema discovery works by executing your files, any module-level side effects run at build time. The fix is to scope discovery to the directories that actually hold schemas using include, and to guard any module that might call process.exit() during import:

zodCompiler({
  include: ["src/schemas/**/*.ts"],
});

Inside such a module you can check process.env.ZOD_COMPILER to detect that you are running under the compiler and skip dangerous side effects.

Hoisting Schemas Out of Hot Functions

One of the most common Zod performance mistakes is defining a schema inside a function that runs often. Every call rebuilds the schema object from scratch. zod-compiler handles this for you with hoisting, which is on by default. A schema written inside a function is lifted to module scope so it is constructed once:

// What you wrote
function validateBody(input: unknown) {
  return z
    .object({ id: z.string().uuid(), title: z.string() })
    .parse(input);
}

// What the compiler effectively produces
const _zh_body = z.object({ id: z.string().uuid(), title: z.string() });
function validateBody(input: unknown) {
  return _zh_body.parse(input);
}

For a React component that defines its form schema inline, or a request handler that builds a schema per request, this alone removes a surprising amount of work, on top of the compilation that makes each validation cheaper.

Where It Sits Among the Alternatives

It is worth being honest about the landscape. Typia compiles validators directly from TypeScript types using a compiler plugin, and it is often the fastest option on the bench, edging ahead on objects and recursive structures. But it asks you to express validation as TypeScript types rather than Zod schemas, so adopting it means rewriting every schema you have, and it does not validate the contents of sets and maps or support transforms. AJV is battle-tested and excellent, especially on the error path, but it lives in the JSON Schema world, so a Zod codebase would have to convert schemas and give up Zod-specific features. TypeBox is fast too, but it is a JSON-Schema-flavored builder with a different developer experience.

The niche zod-compiler carves out is near-Typia performance with full Zod API compatibility. You keep the composable, chainable API you already know, you keep validation of set and map contents, you keep partial compilation for schemas that mix validation and transformation, and crucially you change zero lines of application code. For a team that has already invested in Zod, that is a much easier sell than rewriting everything to chase benchmarks.

A note on maturity: the package is young and moving fast, having shipped many releases in a short span, and weekly download numbers are still modest compared to the giants. Treat it as promising early-adopter tooling. Pin your version, lean on the check command in CI, and verify the behavioral differences against your own tests before you put it on a critical path.

The Takeaway

zod-compiler is the rare performance tool that asks almost nothing of you. You add a bundler plugin, your existing Zod schemas get compiled into flat validation functions with a zero-allocation fast path, and your validation gets faster without a single change to how you write or call your schemas. The two-phase design means you pay almost nothing for the common case of valid input, hoisting cleans up schemas trapped in hot functions, and the diagnostics let you keep an eye on how much is actually compiling. Just read the behavioral-differences section before you ship, decide whether you need stripUnknownKeys, and let the compiler quietly take the runtime cost of your validation down to something close to free.