Compile Zod schemas into zero-overhead validation functions at build time.
No code changes required — keep your existing Zod schemas and get 2-60x faster validation.
Packages
| Package | Description |
|---|---|
| zod-aot | Core compiler — extractor, codegen, runtime fallback, and build plugins |
Quick Start
npm install zod-aot zod@^4
Build Plugin (Recommended)
zod-aot provides build plugins for Vite, webpack, esbuild, Rollup, Rolldown, rspack, rsbuild, Farm, and Bun via unplugin. Two modes: autoDiscover (zero-config) and compile() (explicit opt-in).
autoDiscover (Zero-Config)
Add one line to your build config. Every exported Zod schema in your project is automatically compiled at build time. No wrappers, no imports from zod-aot in your source code.
Step 1 — Install:
Step 2 — Add the plugin to your build config:
// vite.config.ts import { defineConfig } from "vite"; import zodAot from "zod-aot/vite"; export default defineConfig({ plugins: [zodAot({ autoDiscover: true })], });
Step 3 — There is no step 3. Write your Zod schemas as usual:
// src/schemas.ts — plain Zod, no zod-aot import import { z } from "zod"; export const UserSchema = z.object({ name: z.string().min(3), age: z.number().int().positive(), email: z.email(), });
At build time, zod-aot detects the exported UserSchema, compiles it into an optimized validator (3-64x faster), and replaces the export with a /* @__PURE__ */ IIFE that preserves the full Zod prototype chain.
How it works:
- The plugin scans each file for runtime
import ... from "zod"statements - It executes the file and checks each export for
_zod.def(a Zod schema marker) - Discovered schemas go through the same extract → codegen pipeline as
compile()mode - The export assignment (
export const X = z.object(...)) is replaced with an optimized IIFE - The IIFE uses
Object.create(originalSchema)to preserve the full Zod API via prototype chain
Works with any framework that consumes Zod schemas:
// tRPC — no changes to your router code import { UserSchema } from "./schemas"; export const appRouter = t.router({ createUser: t.procedure .input(UserSchema) // automatically 3-64x faster at build time .mutation(({ input }) => createUser(input)), });
// Hono import { zValidator } from "@hono/zod-validator"; import { UserSchema } from "./schemas"; app.post("/users", zValidator("json", UserSchema), (c) => { const user = c.req.valid("json"); return c.json(user); });
// React Hook Form import { zodResolver } from "@hookform/resolvers/zod"; import { UserSchema } from "./schemas"; const form = useForm({ resolver: zodResolver(UserSchema) });
Note: With
autoDiscover, files containing runtime Zod imports are executed at build time. Useincludeto limit scope if your project has files with side effects that also import Zod.zodAot({ autoDiscover: true, include: ["src/schemas"] })
compile() (Explicit Opt-In)
For fine-grained control, wrap specific schemas with compile():
// src/schemas.ts import { z } from "zod"; import { compile } from "zod-aot"; const UserSchema = z.object({ name: z.string().min(3), age: z.number().int().positive(), email: z.email(), }); // Falls back to Zod in dev, replaced with optimized code at build time export const validateUser = compile(UserSchema); validateUser.parse(data); // throws ZodError on failure validateUser.safeParse(data); // { success, data/error }
Both modes can coexist in the same project — compile() schemas are detected first (priority), then autoDiscover picks up remaining plain Zod exports.
Available Plugins
| Build Tool | Import |
|---|---|
| Vite | import zodAot from "zod-aot/vite" |
| webpack | import zodAot from "zod-aot/webpack" |
| esbuild | import zodAot from "zod-aot/esbuild" |
| Rollup | import zodAot from "zod-aot/rollup" |
| Rolldown | import zodAot from "zod-aot/rolldown" |
| rspack | import zodAot from "zod-aot/rspack" |
| rsbuild | import zodAot from "zod-aot/rsbuild" |
| Bun | import zodAot from "zod-aot/bun" |
| Farm | import zodAot from "zod-aot/farm" |
Plugin Options
zodAot({ autoDiscover: true, // auto-detect all exported Zod schemas (no compile() needed) include: ["src/schemas"], // only process files matching these substrings exclude: ["test", "mock"], // skip files matching these substrings zodCompat: true, // use Object.create for Zod API compat (default: true) verbose: true, // log per-schema compilation status and build summary })
Bundle Size & Cross-File Dedup
Generated validators share a small runtime helper layer (__mkv wrapper, issue
factories, well-known regexes for email, uuid, cuid, ipv4, etc.). On
bundlers that support virtual modules — Vite, Rollup, Rolldown, esbuild,
Farm, Bun — the plugin imports these helpers once from
virtual:zod-aot/runtime, so the bundler emits a single bundle-wide copy
regardless of how many files reference them. webpack, rspack, and rsbuild
reject the virtual: URI scheme at the resolver layer, so they import the same
runtime module through a bare-specifier alias (__zod-aot-runtime__) instead —
still a single deduplicated copy per bundle.
The result: a 5-file project with 10 schemas all using z.email() and
z.uuid() produces a bundle where each shared regex appears exactly once.
CLI (Alternative)
If you don't use a bundler, you can generate optimized validation files from the command line:
npx zod-aot generate src/schemas.ts -o src/schemas.compiled.ts npx zod-aot generate src/ -o src/compiled/ npx zod-aot generate src/ --watch
Schema Diagnostics (check)
Analyze schemas for compilation coverage, Fast Path eligibility, and actionable hints — without generating code:
# Tree view with coverage percentage and hints npx zod-aot check src/schemas.ts # JSON output for CI integration npx zod-aot check src/schemas.ts --json # Fail CI if any schema's coverage drops below 80% npx zod-aot check src/schemas.ts --json --fail-under 80
The check command shows:
- Tree view — hierarchical visualization of schema structure with compile/fallback status per node
- Coverage — percentage of schema nodes that are compiled vs. falling back to Zod
- Fast Path eligibility — whether the schema qualifies for two-phase validation, with the specific blocker if ineligible
- Hints — actionable suggestions for fixing fallbacks (e.g., "Replace
.refine()with built-in checks")
See the full documentation for API reference, benchmarks, and usage details.
Benchmarks
5-way comparison: Zod v3 vs Zod v4 vs Zod AOT vs Typia vs AJV
| Scenario | Zod v3 | Zod v4 | Zod AOT | Typia | AJV | vs Zod v4 |
|---|---|---|---|---|---|---|
| simple string | 9.0M | 9.7M | 11.5M | 11.0M | 10.8M | 1.2x |
| string (min/max) | 8.2M | 5.5M | 10.9M | 10.3M | 9.1M | 2.0x |
| number (int+positive) | 8.4M | 5.8M | 10.3M | 11.4M | 10.5M | 1.8x |
| tuple [string, int, bool] | 4.0M | 4.5M | 10.1M | 10.5M | 9.9M | 2.2x |
| discriminatedUnion (3) | 2.3M | 2.8M | 9.9M | 10.1M | 5.6M | 3.5x |
| medium object (valid) | 1.2M | 1.7M | 5.3M | 7.2M | 4.5M | 3.1x |
| large object (10 items) | 81K | 106K | 3.9M | 4.1M | 820K | 37x |
| large object (100 items) | 8.6K | 11.3K | 684K | 818K | 86K | 60x |
| recursive tree (121 nodes) | 23K | 102K | 714K | 1.4M | 249K | 7.0x |
| event log (combined) | 270K | 474K | 4.4M | — | — | 9.2x |
ops/s, higher is better. Measured with vitest bench on Apple M-series. Full results in packages/zod-aot.
Performance Architecture
zod-aot uses a two-phase validation strategy for eligible schemas:
- Fast Path: A single boolean expression chain (
&&) that validates the entire input with zero allocations. On valid input, returns{success: true, data: input}immediately. - Slow Path: Falls back to the existing error-collecting validation if the fast check fails.
Additional optimizations:
- Check ordering: Cheapest checks (length comparisons) run before expensive ones (regex)
- Small enum inlining: Enums with 1-3 values use direct
===comparisons instead ofSet.has() - Pre-compiled regex + Set: Shared across fast and slow paths via preamble declarations
Runtime Support
| Runtime | Version | Status |
|---|---|---|
| Node.js | 22+ | Fully supported |
| Bun | 1.3+ | Fully supported |
| Deno | 2.0+ | Fully supported |
Development
pnpm install
pnpm test
pnpm bench
pnpm lint