Back to Zod

AOT compilation

packages/docs/content/compile.mdx

4.5.44.7 KB
Original Source

import { Callout } from "fumadocs-ui/components/callout";

Under the hood, z.compile() walks the entire schema once and produces a hyperoptimized snippet of flat, loop-free JavaScript that can validate inputs far faster than a standard runtime validator. This snippet can be executed via new Function() (effectively a more powerful eval) to serve as a fast-path validator. Schemas use this to "fast check" validity, falling back to the regular runtime logic on validation failure to provide granular error information. Results and errors are identical to the standard parser.

This can lead to dramatic speedups, especially for container types like objects, arrays, unions, and tuples.

schemaspeedup
20-key object~8x
z.array(z.string()), 10 items~7x
z.array(z.object({…})), 10 items~5.5x
union of 3 objects~5x
tuple of 3~3.5x
discriminated union~3.5x
5-key object~2.8x

There are two ways to opt in.

z.compile()

Compiles a single schema and returns a compiled copy. The original schema is unchanged.

ts
import * as z from "zod";

const Player = z.object({
  username: z.string(),
  bio: z.string(),
  xp: z.number()
});

const CompiledPlayer = z.compile(Player);

A compiled schema is a Zod schema like any other:

  • Same methods: .parse(), .safeParse(), .extend(), .optional(), etc.
  • Same inferred input and output types
  • Same issues and error messages

Methods that derive a new schema (.refine(), .extend(), .optional(), .meta(), …) return uncompiled schemas. Compile the final schema, not an intermediate:

ts
// ❌ the .refine() result is not compiled
const schema = z.compile(z.string()).refine((val) => val.length > 1);

// ✅ compile last
const schema2 = z.compile(z.string().refine((val) => val.length > 1));

import "zod/compile"

Enables compilation globally. Every schema constructed after this import is compiled automatically the first time it parses.

ts
import "zod/compile"; // must come before modules that define schemas
import * as z from "zod";

const schema = z.object({ name: z.string() });
schema.parse({ name: "ok" }); // compiled on first parse

Compilation is lazy, so only the schemas you actually parse with get compiled.

It also works as a Node.js CLI flag, which guarantees it runs before any module defines a schema:

sh
node --import zod/compile app.js   # ESM
node --require zod/compile app.cjs # CommonJS

Or set preload in bunfig.toml or nub.jsonc.

jsonc
{
  "preload": ["zod/compile"]
}

This import is for applications, not libraries.

How it works

<include>../snippets/how-z-compile-works.mdx</include>

On invalid input the fallback runs the uncompiled schema, so the error is the uncompiled schema's error. Two consequences:

  • Invalid inputs pay for both the fast path and the fallback, so compilation does not speed up failures.
  • Refinements and transforms run once on valid input, and at most twice on invalid input.

Unsupported schemas

Some features can't be compiled or don't benefit from compilation. In these cases, z.compile() ejects from compilation and returns the original schema unchanged:

ts
const Schema = z.string().refine(async (val) => isAvailable(val));

z.compile(Schema); // returns Schema itself, uncompiled
  • async refinements, transforms, and checks
  • z.xor()
  • recursive schemas
  • z.coerce.*
  • checks with a custom when
  • .catch() given a callback (.catch(value) with a constant compiles normally)

Inside an object, array, tuple, record, or intersection, an unsupported child runs on the standard parser while the surrounding structure stays compiled. A union with an unsupported member, a .catch() callback, or anything async anywhere in the subtree makes the whole schema fall back.

Encoding (z.encode(), the codec "backward" direction) and async parsing always use the standard parser.

Pass strict to throw instead of falling back — for example, to confirm that a schema on a hot path really did compile:

ts
z.compile(Schema, { strict: true }); // throws ZodCompileAsyncError

ZodCompileAsyncError is thrown for async schemas and ZodCompileUnsupportedError for everything else. Both are thrown only under strict.

Content Security Policy

Compilation uses new Function, which is unavailable in CSP/no-eval environments. Global mode stands down when jitless is set:

ts
z.config({ jitless: true });

Calling z.compile() directly is an explicit opt-in, so it attempts code generation regardless of jitless. Where the environment rejects new Function, the schema comes back uncompiled like any other refusal.