Capítulo 39 de 61

Chapter 39: AOT Compilation

Core Idea

Zod can compile a schema into a specialized fast-path validator (canary-only feature, install zod@canary) that produces identical results and identical errors to the standard parser, with a median 2.4x speedup that scales with how much per-node work the schema does.

Key Concepts

  • z.compile(schema): eagerly compiles one schema, returning a clone with the fast path installed; .parse(), .safeParse(), inference, and composition all work normally on the clone. Methods that derive a new schema (.refine(), .extend(), .optional(), .meta(), ...) return uncompiled schemas — always compile the final schema, not an intermediate one.
  • import "zod/compile": enables compilation globally; every schema constructed after this import compiles lazily on its first parse. Import order matters — place it before any module that defines schemas.
  • Error parity, not a separate path: the fast path only handles the happy path. On any failure (or unsupported construct), Zod falls back to the standard parser, which produces the exact same ZodError — so there's no compiled error format to drift out of sync. Consequence: invalid inputs pay for both the fast path attempt and the fallback, and refinements/transforms may run twice on invalid input (never more than twice; exactly once on valid input).
  • Unsupported constructs throw at compile time: async refinements/transforms (ZodCompileAsyncError); z.xor(), custom when conditions on checks, recursive schemas, z.coerce.*, and .catch() with a callback (vs. a constant, which compiles fine) all raise ZodCompileUnsupportedError. Inside containers, an unsupported child schema just runs on the standard parser while the rest stays compiled — except a callback-based .catch(), which blocks compilation of the whole schema it's attached to.
  • CSP environments: compilation uses new Function, unavailable under strict CSP. z.config({ jitless: true }) disables global-mode compilation automatically; calling z.compile() directly in a jitless environment throws.

Code Examples

const Player = z.object({ username: z.string(), xp: z.number() });
const CompiledPlayer = z.compile(Player); // fast-path clone; Player itself is unchanged

// compile LAST — refine() on an already-compiled schema returns an uncompiled result
const wrong = z.compile(z.string()).refine((val) => val.length > 1); // not compiled
const right = z.compile(z.string().refine((val) => val.length > 1)); // compiled

import "zod/compile"; // before any schema-defining module
  • What it demonstrates: the two opt-in mechanisms (explicit per-schema vs. global lazy) and the "compile last" ordering rule.

Reference Tables

Schema shapeApproximate speedup
z.array(z.string()), 100 items~14x
20-key object~9x
discriminated union~4x
5-key object~2.5x
bare z.string()none (no dispatch to remove)

Anti-patterns

  • Compiling before chaining .refine()/.extend()/etc.: those methods return a new, uncompiled schema — compile only the final version.
  • Enabling import "zod/compile" global mode inside a library: it silently affects every consumer's schemas; leave that opt-in decision to the application, not a library dependency.

Key Takeaways

  1. Compilation pays off most for schemas that do a lot of per-node work (large arrays, wide objects); a bare z.string() gains nothing.
  2. It's transparent and safe by design — any input that fails, or any construct compilation can't model, falls back to the exact same standard-parser behavior and errors.
  3. It's a canary-only, opt-in feature — not yet in a stable release as of this doc's fetch date.

Connects To

  • Basic Usage — Define, Parse, Handle Errors, Infer: .parse()/.safeParse() behave identically on compiled and uncompiled schemas.
  • Exclusive unions (XOR): one of the constructs AOT compilation can't currently model.