wakita181009/zod-aot

34

stars

289

commits

TypeScript

primary language

Sep 11, 2026

updated

README

Zod AOT

Compile Zod schemas into zero-overhead validation functions at build time.

CI codecov npm License: MIT

No code changes required — keep your existing Zod schemas and get 2-60x faster validation.

Packages

PackageDescription
zod-aotCore compiler — extractor, codegen, runtime fallback, and build plugins

Quick Start

npm install zod-aot zod@^4

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:

npm install -D zod-aot

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:

  1. The plugin scans each file for runtime import ... from "zod" statements
  2. It executes the file and checks each export for _zod.def (a Zod schema marker)
  3. Discovered schemas go through the same extract → codegen pipeline as compile() mode
  4. The export assignment (export const X = z.object(...)) is replaced with an optimized IIFE
  5. 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. Use include to 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 ToolImport
Viteimport zodAot from "zod-aot/vite"
webpackimport zodAot from "zod-aot/webpack"
esbuildimport zodAot from "zod-aot/esbuild"
Rollupimport zodAot from "zod-aot/rollup"
Rolldownimport zodAot from "zod-aot/rolldown"
rspackimport zodAot from "zod-aot/rspack"
rsbuildimport zodAot from "zod-aot/rsbuild"
Bunimport zodAot from "zod-aot/bun"
Farmimport 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

ScenarioZod v3Zod v4Zod AOTTypiaAJVvs Zod v4
simple string9.0M9.7M11.5M11.0M10.8M1.2x
string (min/max)8.2M5.5M10.9M10.3M9.1M2.0x
number (int+positive)8.4M5.8M10.3M11.4M10.5M1.8x
tuple [string, int, bool]4.0M4.5M10.1M10.5M9.9M2.2x
discriminatedUnion (3)2.3M2.8M9.9M10.1M5.6M3.5x
medium object (valid)1.2M1.7M5.3M7.2M4.5M3.1x
large object (10 items)81K106K3.9M4.1M820K37x
large object (100 items)8.6K11.3K684K818K86K60x
recursive tree (121 nodes)23K102K714K1.4M249K7.0x
event log (combined)270K474K4.4M9.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:

  1. Fast Path: A single boolean expression chain (&&) that validates the entire input with zero allocations. On valid input, returns {success: true, data: input} immediately.
  2. 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 of Set.has()
  • Pre-compiled regex + Set: Shared across fast and slow paths via preamble declarations

Runtime Support

RuntimeVersionStatus
Node.js22+Fully supported
Bun1.3+Fully supported
Deno2.0+Fully supported

Development

pnpm install
pnpm test
pnpm bench
pnpm lint

License

MIT

Contributors

wakita181009

193 commits

dependabot[bot]

83 commits

claude

2 commits

wakita181009/zod-aot

34

stars

289

commits

TypeScript

primary language

Sep 11, 2026

updated

README

Zod AOT

Compile Zod schemas into zero-overhead validation functions at build time.

CI codecov npm License: MIT

No code changes required — keep your existing Zod schemas and get 2-60x faster validation.

Packages

PackageDescription
zod-aotCore compiler — extractor, codegen, runtime fallback, and build plugins

Quick Start

npm install zod-aot zod@^4

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:

npm install -D zod-aot

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:

  1. The plugin scans each file for runtime import ... from "zod" statements
  2. It executes the file and checks each export for _zod.def (a Zod schema marker)
  3. Discovered schemas go through the same extract → codegen pipeline as compile() mode
  4. The export assignment (export const X = z.object(...)) is replaced with an optimized IIFE
  5. 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. Use include to 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 ToolImport
Viteimport zodAot from "zod-aot/vite"
webpackimport zodAot from "zod-aot/webpack"
esbuildimport zodAot from "zod-aot/esbuild"
Rollupimport zodAot from "zod-aot/rollup"
Rolldownimport zodAot from "zod-aot/rolldown"
rspackimport zodAot from "zod-aot/rspack"
rsbuildimport zodAot from "zod-aot/rsbuild"
Bunimport zodAot from "zod-aot/bun"
Farmimport 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

ScenarioZod v3Zod v4Zod AOTTypiaAJVvs Zod v4
simple string9.0M9.7M11.5M11.0M10.8M1.2x
string (min/max)8.2M5.5M10.9M10.3M9.1M2.0x
number (int+positive)8.4M5.8M10.3M11.4M10.5M1.8x
tuple [string, int, bool]4.0M4.5M10.1M10.5M9.9M2.2x
discriminatedUnion (3)2.3M2.8M9.9M10.1M5.6M3.5x
medium object (valid)1.2M1.7M5.3M7.2M4.5M3.1x
large object (10 items)81K106K3.9M4.1M820K37x
large object (100 items)8.6K11.3K684K818K86K60x
recursive tree (121 nodes)23K102K714K1.4M249K7.0x
event log (combined)270K474K4.4M9.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:

  1. Fast Path: A single boolean expression chain (&&) that validates the entire input with zero allocations. On valid input, returns {success: true, data: input} immediately.
  2. 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 of Set.has()
  • Pre-compiled regex + Set: Shared across fast and slow paths via preamble declarations

Runtime Support

RuntimeVersionStatus
Node.js22+Fully supported
Bun1.3+Fully supported
Deno2.0+Fully supported

Development

pnpm install
pnpm test
pnpm bench
pnpm lint

License

MIT

Contributors

wakita181009

193 commits

dependabot[bot]

83 commits

claude

2 commits

Languages

TypeScript

98.7%

JavaScript

1.3%