💎 Zod 4 is now stable!  Read the announcement.
Zod logo

AOT compilation

Edit this page

For hot validation paths, Zod can compile a schema into a specialized validator ahead of time, with identical results and identical errors.

Across a 55-schema benchmark the median speedup is 2.4x. It scales with how much work the schema does per parse, because what compilation removes is per-node dispatch and allocation rather than the checks themselves:

schemaspeedup
z.array(z.string()), 100 items~14x
z.array(z.object({…})), 50 items~9x
20-key object~9x
nested object~4.5x
discriminated union~4x
5-key object~2.5x
z.string().min(3).max(64).regex(…)~2.8x
bare z.string()none

A schema whose entire body is one typeof has no dispatch to remove, so compiling it buys nothing. Everything built out of such schemas still benefits — leaves are inlined into their parent's compiled code.

There are two ways to opt in.

z.compile()

Compiles a single schema eagerly and returns a clone with the fast path installed.

import * as z from "zod";
 
const Player = z.object({
  username: z.string(),
  xp: z.number(),
});
 
const CompiledPlayer = z.compile(Player);
 
CompiledPlayer.parse({ username: "billie", xp: 100 });
CompiledPlayer.safeParse({ username: "billie", xp: 100 });

The clone is a regular Zod schema — .parse(), .safeParse(), type inference, Standard Schema integration, and composition all work as usual. The original schema is unchanged.

One thing to keep in mind: methods that derive a new schema (.refine(), .extend(), .optional(), .meta(), …) return uncompiled schemas. Compile the final schema, not an intermediate:

// ❌ 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 on its first parse.

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

Module evaluation order matters: schemas constructed before the import are not affected. Place the import at the top of your application entry point.

Compilation is lazy (first parse), so intermediate schemas created during builder chains cost nothing extra.

Error parity

The compiled fast path is a happy-path validator. When an input fails validation — or requires anything the fast path doesn't support — Zod transparently falls back to the standard parser, which produces the exact same ZodError as the uncompiled schema. There is no separate compiled error path to drift out of sync.

Two consequences worth knowing:

  • Invalid inputs pay for both the fast path and the fallback. If your workload is dominated by invalid inputs, compilation won't help (it also won't change any behavior).
  • Refinements and transforms may run twice on invalid input — once in the fast path, once in the fallback. They run exactly once on valid input, and never more than twice.

Unsupported schemas

Compilation is sync and forward-only. z.compile() throws if the schema can't be fully modeled:

  • ZodCompileAsyncError — the schema contains an async refinement, transform, or check.
  • ZodCompileUnsupportedError — the schema uses a construct the fast path can't reproduce exactly:
    • z.xor(), whose exactly-one-match rule needs every branch to reject for the same reason the standard parser would.
    • Checks with custom when conditions.
    • A recursive schema, which needs the standard parser to resolve reference cycles in the input.
    • Coercion (z.coerce.*), which the fast path does not model.
    • .catch() given a callback. A callback receives issues finalized against the caller's error map, which compiled code never sees. .catch(value) with a constant compiles normally.

Inside containers (objects, arrays, tuples, records, intersections), an unsupported child doesn't prevent compilation — the child runs on the standard parser while the surrounding structure stays compiled.

Under global mode, schemas that fail to compile silently keep using the standard parser. There is no behavioral difference — only the speedup is lost. For this reason, avoid enabling global mode in a library on behalf of your users; leave the choice to the application.

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

Content Security Policy

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

z.config({ jitless: true });

Calling z.compile() directly is treated as an explicit opt-in and throws ZodCompileUnsupportedError if code generation is unavailable.

On this page