zod-compiler 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,23 +2,43 @@
2
2
 
3
3
  **Compile Zod schemas into zero-overhead validation functions at build time.**
4
4
 
5
- Keep your existing Zod schemas. Get **1.1-46x faster** validation, and up to **41x** on rejected
5
+ Keep your existing Zod schemas. Get **up to 42x faster** validation, and up to **47x** on rejected
6
6
  input. No code changes required.
7
7
 
8
- Requires **Zod ≥ 4.5**. The compiled output reproduces 4.5's semantics exactly code-point string
9
- lengths, symbol-keyed shapes, tuple issue order so it does not match earlier 4.x releases. Stay on
10
- zod-compiler 1.x if you are on Zod 4.0–4.4.
8
+ Requires **Zod ≥ 4.5**. Compiled output reproduces 4.5's semantics exactly, down to code-point string
9
+ lengths, symbol-keyed shapes and tuple issue order, so it does not match earlier 4.x releases. Stay on
10
+ zod-compiler 1.x for Zod 4.0–4.4.
11
11
 
12
12
  - [What Gets Compiled](#what-gets-compiled)
13
13
  - [Schema Hoisting](#schema-hoisting)
14
+ - [z.compile vs zod-compiler](#zcompile-vs-zod-compiler)
14
15
  - [Benchmark](#benchmark)
15
16
 
16
17
  > [!NOTE]
17
18
  > zod-compiler has been tested to work in large projects with tens of thousands of Zod schemas.
18
19
 
20
+ ## z.compile vs zod-compiler
21
+
22
+ Both generate optimized JavaScript. Zod's [`z.compile()`](https://zod.dev/compile) does it at runtime
23
+ with `new Function()` (JIT); zod-compiler's plugins and CLI do it at build time (AOT), so production
24
+ loads pre-generated validators.
25
+
26
+ | | zod-compiler (build plugins / CLI) | Zod `z.compile()` |
27
+ | -------------------------------------- | -------------------------------------------- | --------------------------------------------- |
28
+ | Compilation | Build time (true AOT) | Runtime (`z.compile()` or the first parse) |
29
+ | Reported validation speedup | Up to 42x; up to 47x on rejected input | ~9x in Zod's headline example |
30
+ | Uses `new Function()` at runtime\* | No | Yes |
31
+ | Cold start | Fast; the validator is already generated | Pays for code generation at startup/first use |
32
+ | Strict CSP without `'unsafe-eval'` | Supported | Compilation is unavailable |
33
+ | Compiler shipped in the runtime bundle | No; only validators and runtime helpers ship | Yes; about 7 KB gzipped according to Zod |
34
+
35
+ \*zod-compiler's optional [`jit()`](#4-runtime-compilation-no-build-step) and
36
+ [Node.js register hook](#5-nodejs-register-hook) use `new Function()` and have the same runtime
37
+ code-generation and CSP trade-offs as Zod's `z.compile()`.
38
+
19
39
  ## Usage
20
40
 
21
- Five ways to use zod-compiler pick one:
41
+ Five ways to use zod-compiler. Pick one:
22
42
 
23
43
  ### 1. Automatic Mode (Default)
24
44
 
@@ -50,7 +70,7 @@ export const CreateUserSchema = z.object({
50
70
  Use them as usual. Methods are installed on the original schema object, so `.shape`, `._zod`, Standard
51
71
  Schema, `instanceof` and `z.toJSONSchema()` keep working.
52
72
 
53
- Compiled schemas also expose **`.is(input): input is T`** a zero-allocation drop-in for
73
+ Compiled schemas also expose **`.is(input): input is T`**, a zero-allocation drop-in for
54
74
  `safeParse(x).success`.
55
75
 
56
76
  ### 2. compile() (Explicit)
@@ -74,7 +94,8 @@ validateUser.parse(data);
74
94
  validateUser.safeParse(data);
75
95
  ```
76
96
 
77
- `compile()` and auto mode coexist. Pair with `schemas: "explicit"` to make `compile()` the _only_ path — no automatic detection, no build-time execution of plain schema files.
97
+ `compile()` and auto mode coexist. Pair with `schemas: "explicit"` to make `compile()` the _only_ path:
98
+ no automatic detection, no build-time execution of plain schema files.
78
99
 
79
100
  ### 3. CLI (No Bundler)
80
101
 
@@ -99,7 +120,7 @@ npx zod-compiler generate src/ --emit compact
99
120
 
100
121
  ### 4. Runtime Compilation (No Build Step)
101
122
 
102
- `jit()` runs the same pipeline in-process, for `tsx`, `ts-node`, Jest anywhere no plugin fires:
123
+ `jit()` runs the same pipeline in-process for `tsx`, `ts-node`, Jest, and anywhere else no plugin fires:
103
124
 
104
125
  ```typescript
105
126
  import { jit } from "zod-compiler/jit";
@@ -108,12 +129,12 @@ export const UserSchema = jit(z.object({ name: z.string().min(1), email: z.email
108
129
  ```
109
130
 
110
131
  Same validators a build emits, installed on the schema object, so Zod interop is unchanged.
111
- Compilation is lazy 0.1-0.3 ms on a schema's first parse; `{ eager: true }` compiles up front,
112
- `jitAll(namespace)` takes a whole module.
132
+ Compilation is lazy, costing 0.1-0.3 ms on a schema's first parse. `{ eager: true }` compiles up front
133
+ and `jitAll(namespace)` takes a whole module.
113
134
 
114
135
  The cost is the import: ~570 KB of codegen and `acorn`, **~10 ms of module load**. That suits a
115
- long-lived process, not a CLI, a cold serverless handler or a browser use the build plugin there.
116
- Libraries should ship plain Zod and let the app decide.
136
+ long-lived process, not a CLI, a cold serverless handler or a browser. Use the build plugin there, and
137
+ have libraries ship plain Zod so the app can decide.
117
138
 
118
139
  Needs `new Function`, as Zod's own object fast-path does. `z.config({ jitless: true })` and a CSP
119
140
  that blocks eval both leave a working plain-Zod schema.
@@ -134,11 +155,11 @@ also chains with TypeScript runners:
134
155
  node --import zod-compiler/register --import tsx src/server.ts
135
156
  ```
136
157
 
137
- This is runtime JIT instrumentation, not the AOT source rewriting performed by the Vite, Rsbuild, and
138
- other build plugins. The hook identifies exported schema bindings and registers their live Zod objects;
139
- validators are generated in-process, lazily on first use. It does not execute modules twice and adds no
140
- transform cache beyond Node's module cache. Use a build plugin or the CLI when generated validator code
141
- must exist before Node starts or runtime `new Function` is unavailable.
158
+ This is runtime JIT instrumentation, not the AOT source rewriting the build plugins perform. The hook
159
+ registers the live Zod objects behind exported schema bindings and generates validators in-process on
160
+ first use. It does not execute modules twice, and adds no cache beyond Node's own module cache. Use a
161
+ build plugin or the CLI when validator code must exist before Node starts, or when `new Function` is
162
+ unavailable at runtime.
142
163
 
143
164
  Optional settings come from `zod-compiler.json` in the working directory:
144
165
 
@@ -154,9 +175,9 @@ Optional settings come from `zod-compiler.json` in the working directory:
154
175
  }
155
176
  ```
156
177
 
157
- `output: "compact"` preserves the Zod schema and compiled valid-input fast path while delegating cold
158
- error production to Zod. Full `"schema"` output remains the default. `"bag"` is unavailable because a
159
- load hook cannot replace already-linked ESM export bindings safely.
178
+ `output: "compact"` keeps the Zod schema and the compiled fast path, delegating cold error production
179
+ to Zod. Full `"schema"` output stays the default. `"bag"` is unavailable here: a load hook cannot safely
180
+ replace already-linked ESM export bindings.
160
181
 
161
182
  ## Build Plugin
162
183
 
@@ -176,8 +197,8 @@ load hook cannot replace already-linked ESM export bindings safely.
176
197
  | Bun | `import zodCompiler from "zod-compiler/bun"` |
177
198
  | Farm | `import zodCompiler from "zod-compiler/farm"` |
178
199
 
179
- Turbopack takes a loader rather than a plugin see [Next.js (Turbopack)](#nextjs-turbopack). Metro
180
- has neither see [React Native / Expo](#react-native--expo).
200
+ Turbopack takes a loader rather than a plugin; see [Next.js (Turbopack)](#nextjs-turbopack). Metro has
201
+ neither; see [React Native / Expo](#react-native--expo).
181
202
 
182
203
  ### Options
183
204
 
@@ -186,13 +207,13 @@ has neither — see [React Native / Expo](#react-native--expo).
186
207
  | `schemas` | `"auto" \| "explicit"` | `"auto"` | `"auto"` compiles every exported schema (and hoisted in-function ones); `"explicit"` only `compile()` calls |
187
208
  | `include` | `string[]` | — | Only process files matching these path globs |
188
209
  | `exclude` | `string[]` | — | Skip files matching these path globs |
189
- | `output` | `"schema" \| "bag" \| "compact"` | `"schema"` | What a compiled export evaluates to see [Compact Output](#compact-output-output-compact) |
210
+ | `output` | `"schema" \| "bag" \| "compact"` | `"schema"` | What a compiled export evaluates to; see [Compact Output](#compact-output-output-compact) |
190
211
  | `verbose` | `boolean` | `false` | Log per-schema compilation status |
191
- | `hoist` | `boolean` | `true` | Move schemas built inside functions to module scope see [Schema Hoisting](#schema-hoisting) |
212
+ | `hoist` | `boolean` | `true` | Move schemas built inside functions to module scope; see [Schema Hoisting](#schema-hoisting) |
192
213
  | `apply` | `"build" \| "serve" \| "all"` | builds + Vitest | **Vite only**: when the plugin runs |
193
- | `codegenMode` | `"lean" \| "inline"` | auto | `"inline"` emits helpers per file; needed for transpile-only esbuild see [SWC](#swc) |
214
+ | `codegenMode` | `"lean" \| "inline"` | auto | `"inline"` emits helpers per file; needed for transpile-only esbuild (see [SWC](#swc)) |
194
215
  | `cache` | `boolean \| string` | `true` | Persistent transform cache in `node_modules/.cache/zod-compiler` |
195
- | `parallel` | `boolean \| number` | `false` | Run transforms on worker threads see [Parallel Transforms](#parallel-transforms) |
216
+ | `parallel` | `boolean \| number` | `false` | Run transforms on worker threads; see [Parallel Transforms](#parallel-transforms) |
196
217
 
197
218
  ```typescript
198
219
  zodCompiler({
@@ -212,10 +233,9 @@ export default defineConfig({
212
233
  });
213
234
  ```
214
235
 
215
- > **Note:** Vitest is detected automatically (via the `VITEST` env var), so
216
- > tests compile and exercise the same validators that ship to production
217
- > including their performance. Pass `apply: "build"` if you want tests to use
218
- > the plain Zod fallback instead.
236
+ > **Note:** Vitest is detected automatically (via the `VITEST` env var), so tests exercise the same
237
+ > validators that ship to production, performance included. Pass `apply: "build"` to have tests use the
238
+ > plain Zod fallback instead.
219
239
 
220
240
  ### Bun
221
241
 
@@ -227,7 +247,7 @@ import zodCompiler from "zod-compiler/bun";
227
247
  await Bun.build({ entrypoints: ["./src/index.tsx"], outdir: "./dist", plugins: [zodCompiler()] });
228
248
  ```
229
249
 
230
- For code run straight from source (`bun run src/server.ts`) no build plugin fires — use
250
+ No build plugin fires for code run straight from source (`bun run src/server.ts`). Use
231
251
  [`jit()`](#4-runtime-compilation-no-build-step) to compile in-process, or the
232
252
  [CLI](#3-cli-no-bundler) to compile ahead of time.
233
253
 
@@ -247,22 +267,22 @@ function getSchema() {
247
267
  ```
248
268
 
249
269
  Only expressions built from imported bindings and literals move; anything touching locals, `this` or
250
- `new Date()` stays put. Combinator chains on imported schemas qualify via `schemaNamePattern`
251
- (default `/ZodSchema$/`).
270
+ `new Date()` stays put. Combinator chains on imported schemas qualify via `schemaNamePattern` (default
271
+ `/ZodSchema$/`).
252
272
 
253
- In auto mode hoisted schemas also **compile**, rescuing the schema that never leaves a function (a
254
- slonik query, a tRPC input) and so is invisible to export scanning: ~16,700 ns → ~14 ns per call.
273
+ In auto mode hoisted schemas also **compile**, rescuing schemas that never leave a function (a slonik
274
+ query, a tRPC input) and are therefore invisible to export scanning: ~16,700 ns → ~14 ns per call.
255
275
 
256
276
  ### Bundle Size & Cross-File Dedup
257
277
 
258
278
  Validators share a runtime helper layer imported from one module, so each helper appears once per
259
- bundle. Schemas in a file sharing a structurally identical sub-shape emit its error walk once
260
- **19-28% raw / 10-18% gzipped**, scaling with how much the file repeats.
279
+ bundle. Schemas in a file that share a structurally identical sub-shape emit its error walk once, worth
280
+ **19-28% raw / 10-18% gzipped** and scaling with how much the file repeats.
261
281
 
262
282
  Build plugins serve that module from a resolve hook (`virtual:zod-compiler/runtime`, or
263
283
  `__zod-compiler-runtime__` on webpack and rspack, which reject the `virtual:` scheme). A loader host
264
284
  has no hook, so [Turbopack](#nextjs-turbopack) imports the same code from the real subpath
265
- `zod-compiler/runtime` instead opt-in there, since it only pays off where the host bundles that
285
+ `zod-compiler/runtime` instead. It is opt-in there, since it only pays off where the host bundles that
266
286
  import rather than leaving it external.
267
287
 
268
288
  **Transpile-only esbuild builds** (no `--bundle`) never fire the bundler's resolve hooks, so the
@@ -277,7 +297,7 @@ Set `output: "bag"` to also drop the retained Zod schema when you don't need `.s
277
297
 
278
298
  ### Next.js (Turbopack)
279
299
 
280
- Turbopack the default since Next.js 16 [runs webpack loaders but no webpack
300
+ Turbopack, the default since Next.js 16, [runs webpack loaders but no webpack
281
301
  plugins](https://nextjs.org/docs/app/api-reference/turbopack#webpack-plugins), so use the loader
282
302
  entry point:
283
303
 
@@ -304,20 +324,20 @@ const nextConfig: NextConfig = {
304
324
  export default nextConfig;
305
325
  ```
306
326
 
307
- Automatic mode, unchanged sources, `next dev` and `next build`. Options go in the object form
308
- `loaders: [{ loader: "zod-compiler/turbopack", options: { verbose: true } }]` and must be plain
309
- JSON, so `hoist.schemaNamePattern` takes a string, not a RegExp.
327
+ Automatic mode, unchanged sources, `next dev` and `next build`. Options go in the object form,
328
+ `loaders: [{ loader: "zod-compiler/turbopack", options: { verbose: true } }]`, and must be plain JSON,
329
+ so `hoist.schemaNamePattern` takes a string, not a RegExp.
310
330
 
311
331
  Three things worth knowing:
312
332
 
313
333
  - **Keep the `content` pattern loose.** Narrowing it to `"zod"` skips `zod/v4`, `zod/mini` and the
314
- `zod-compiler` import behind `schemas: "explicit"` those files just quietly stay uncompiled.
334
+ `zod-compiler` import behind `schemas: "explicit"`, leaving those files quietly uncompiled.
315
335
  - **`codegenMode: "lean"` is App-Router-only.** It shares one copy of the helpers across the bundle,
316
336
  but Pages Router server code externalizes `node_modules` imports unless
317
337
  [`bundlePagesRouterDependencies`](https://nextjs.org/docs/pages/api-reference/config/next-config-js/bundlePagesRouterDependencies)
318
338
  is on, so a devDependency install throws `ERR_MODULE_NOT_FOUND` in production.
319
- - **A `"use server"` file can only export async functions**, so keep schemas there inside a function
320
- [hoisting](#schema-hoisting) still compiles them. `"use client"` modules need nothing special.
339
+ - **A `"use server"` file can only export async functions**, so keep schemas there inside a function.
340
+ [Hoisting](#schema-hoisting) still compiles them. `"use client"` modules need nothing special.
321
341
 
322
342
  Turbopack caches loader results itself, so cache `.next/cache` in CI rather than
323
343
  `node_modules/.cache/zod-compiler`. `next dev --webpack` / `next build --webpack` still work, with
@@ -343,25 +363,25 @@ Defaults `codegenMode` to `"inline"` (SWC has no virtual-module hook); pass
343
363
 
344
364
  ### React Native / Expo
345
365
 
346
- There is no Metro plugin unplugin has no Metro adapter. Use the [CLI](#3-cli-no-bundler); Metro
366
+ There is no Metro plugin, since unplugin has no Metro adapter. Use the [CLI](#3-cli-no-bundler); Metro
347
367
  bundles what it emits as ordinary source:
348
368
 
349
369
  ```bash
350
370
  npx zod-compiler generate src/schemas/ -o src/schemas/compiled/ --watch
351
371
  ```
352
372
 
353
- Worth the step: **Hermes ships no JIT and no `new Function`**, so Zod's own object fast path is
354
- unavailable on device and [`jit()`](#4-runtime-compilation-no-build-step) cannot run there at all.
373
+ The step pays for itself: **Hermes ships no JIT and no `new Function`**, so Zod's own object fast path
374
+ is unavailable on device and [`jit()`](#4-runtime-compilation-no-build-step) cannot run there at all.
355
375
 
356
- Keep schema modules free of `react-native` and `expo-*` imports, transitively discovery executes
357
- each file and its import graph in Node (in both modes), and one that throws falls back to runtime
358
- Zod silently.
376
+ Keep schema modules free of `react-native` and `expo-*` imports, transitively. Discovery executes each
377
+ file and its import graph in Node (in both modes), and one that throws falls back to runtime Zod
378
+ silently.
359
379
 
360
380
  ### Compact Output (`output: "compact"`)
361
381
 
362
382
  Compiles the fast path and delegates the cold error path to the retained Zod schema, dropping
363
- **~73% raw / ~71% gzipped** on 50 distinct schemas. The hot path is unchanged and errors are Zod's own;
364
- only reading `.error` invokes Zod. Mutually exclusive with `output: "bag"`.
383
+ **~73% raw / ~71% gzipped** across 50 distinct schemas. The hot path is unchanged and errors are Zod's
384
+ own; only reading `.error` invokes Zod. Mutually exclusive with `output: "bag"`.
365
385
 
366
386
  ```typescript
367
387
  zodCompiler({ output: "compact" });
@@ -369,18 +389,16 @@ zodCompiler({ output: "compact" });
369
389
 
370
390
  ### Workers and Serverless Startup
371
391
 
372
- Workers often construct every imported schema during module initialization, even when an isolate only
373
- validates a few of them. Compiling all of those schemas can improve validation while increasing bundle
374
- size and startup work. Compact output reduces compiler-generated error-path code, but still retains the
375
- original Zod schema and does not make eager schema construction lazy.
376
-
377
- Automatic discovery remains the default. If an application has a clear schema boundary, narrow
378
- `include` or use `schemas: "explicit"` to avoid compiling intermediate exports.
392
+ Workers construct every imported schema at module init, even when an isolate validates only a few.
393
+ Compiling all of them buys validation speed at the cost of bundle size and startup work. Compact output
394
+ trims the generated error path but still retains the Zod schema, and does not make eager construction
395
+ lazy.
379
396
 
380
- Use `output: "bag"` only when consumers do not need Zod APIs such as `.shape`, `.extend()`, `.meta()`,
381
- or `z.toJSONSchema()`; it can omit the retained schema entirely.
397
+ If your app has a clear schema boundary, narrow `include` or use `schemas: "explicit"` to skip
398
+ intermediate exports. Use `output: "bag"` only where consumers need no Zod APIs (`.shape`, `.extend()`,
399
+ `.meta()`, `z.toJSONSchema()`); it can drop the retained schema entirely.
382
400
 
383
- Measure startup separately from validation throughput using the target deployment and bundle.
401
+ Measure startup separately from validation throughput, on the target deployment and bundle.
384
402
 
385
403
  ### Auto Mode: Side Effects Warning
386
404
 
@@ -388,8 +406,8 @@ Auto mode executes files to inspect their exports, so a file with schema-shaped
388
406
  effects runs them at build time. Limit the scan with `include`.
389
407
 
390
408
  For the common `env.ts` that validates `process.env` and exits, zod-compiler sets
391
- `process.env.ZOD_COMPILER` during discovery and intercepts `process.exit`, so the build never crashes
392
- those files just fall back to runtime Zod. To keep them compiled, guard on it:
409
+ `process.env.ZOD_COMPILER` during discovery and intercepts `process.exit`, so the build never crashes.
410
+ Those files fall back to runtime Zod. To keep them compiled, guard on it:
393
411
 
394
412
  ```typescript
395
413
  if (!process.env.ZOD_COMPILER) {
@@ -400,12 +418,12 @@ if (!process.env.ZOD_COMPILER) {
400
418
  With `@t3-oss/env-*`, pass `skipValidation: !!process.env.ZOD_COMPILER`.
401
419
 
402
420
  A schema whose SHAPE branches on an env var is baked at build time, and the cache key does not include
403
- the environment give each environment its own `cache` directory if you share one across them.
421
+ the environment. Give each environment its own `cache` directory if you share one across them.
404
422
 
405
423
  ### Large projects and CI
406
424
 
407
425
  Discovery executes each schema file inside the bundler's process, so the **first cold run** is the
408
- expensive one later runs hit the persistent cache.
426
+ expensive one. Later runs hit the persistent cache.
409
427
 
410
428
  ```yaml
411
429
  - uses: actions/cache@v4
@@ -419,10 +437,10 @@ never mention `zod` cost nothing.
419
437
 
420
438
  ### Parallel Transforms
421
439
 
422
- Discovery runs one file at a time on the bundler's own thread executions are serialized so
423
- concurrent transforms cannot double-execute a shared dependency. `parallel` moves whole transforms
424
- onto worker threads instead, each with its own loader and module cache, which is what makes running
425
- them at the same time sound.
440
+ Discovery runs one file at a time on the bundler's own thread, serializing executions so concurrent
441
+ transforms cannot double-execute a shared dependency. `parallel` moves whole transforms onto worker
442
+ threads instead, each with its own loader and module cache, which is what makes running them at the
443
+ same time sound.
426
444
 
427
445
  ```typescript
428
446
  zodCompiler({ parallel: true }); // one worker per core, less one, capped at 4
@@ -431,34 +449,34 @@ zodCompiler({ parallel: 2 }); // or pick the count yourself
431
449
 
432
450
  **Whether it pays depends on your import graph, not your core count.** A module shared by many
433
451
  schema files is executed once in-process and once _per worker_ here. Files with independent graphs
434
- win; files chained through each other can lose. Both rows below are 120 files of 8 schemas each, on
435
- 12 performance cores the only difference is whether the files import one another:
452
+ win; files chained through each other can lose. Both rows below are 120 files of 8 schemas each on
453
+ 12 performance cores, differing only in whether the files import one another:
436
454
 
437
455
  | Transform (120 files) | in-process | n=2 | n=4 | n=8 | n=12 |
438
456
  | --------------------- | ---------: | -------: | -------: | -------: | -------: |
439
457
  | independent graphs | 3,633 ms | 2,263 ms | 1,508 ms | 1,786 ms | 2,119 ms |
440
458
  | 120-deep import chain | 945 ms | 977 ms | 1,045 ms | 1,796 ms | 3,332 ms |
441
459
 
442
- So measure before adopting it `ZOD_COMPILER_TIMING=1` prints the per-phase breakdown, and the
443
- `discover` line is the one workers move. Throughput peaks around four workers and declines past it:
444
- beyond that point every extra worker re-executes more graph, holds another copy of it in memory, and
445
- adds to the generated source that the single receiving thread has to deserialize.
460
+ So measure before adopting it. `ZOD_COMPILER_TIMING=1` prints the per-phase breakdown, and `discover`
461
+ is the line workers move. Throughput peaks around four workers and declines past it: every extra worker
462
+ re-executes more graph, holds another copy in memory, and adds to the generated source that the single
463
+ receiving thread has to deserialize.
446
464
 
447
- Emitted code, sourcemaps and cache entries are identical either way `parallel` is not part of the
448
- cache key, so a parallel build and a serial one share the same cache. The disk cache and dependency
449
- crawling stay on the bundler thread, and if a worker cannot start or dies mid-build its file is
450
- retried in-process rather than failing the build.
465
+ Emitted code, sourcemaps and cache entries are identical either way. `parallel` is not part of the
466
+ cache key, so parallel and serial builds share one cache. Disk caching and dependency crawling stay on
467
+ the bundler thread, and a worker that cannot start or dies mid-build has its file retried in-process
468
+ rather than failing the build.
451
469
 
452
- A **warm cache still beats parallelism**, and costs no memory reach for `parallel` for the cold
453
- runs the cache cannot help with.
470
+ A **warm cache still beats parallelism** and costs no memory. Reach for `parallel` on the cold runs the
471
+ cache cannot help with.
454
472
 
455
473
  ## Framework Examples
456
474
 
457
- Nothing framework-specific is needed exported schemas are compiled in place, so anything accepting
458
- a Zod schema picks up the compiled version:
475
+ Nothing framework-specific is needed. Exported schemas are compiled in place, so anything accepting a
476
+ Zod schema picks up the compiled version:
459
477
 
460
478
  ```typescript
461
- // tRPC no .input(compile(...)) needed
479
+ // tRPC: no .input(compile(...)) needed
462
480
  t.procedure.input(CreateUserSchema).mutation(({ input }) => createUser(input));
463
481
 
464
482
  // Hono
@@ -468,7 +486,7 @@ app.post("/users", zValidator("json", UserSchema), (c) => c.json(c.req.valid("js
468
486
  useForm({ resolver: zodResolver(SignupSchema) });
469
487
  ```
470
488
 
471
- The same applies to any [Standard Schema](https://standardschema.dev) consumer `~standard.validate`
489
+ The same applies to any [Standard Schema](https://standardschema.dev) consumer: `~standard.validate`
472
490
  routes through the compiled validator.
473
491
 
474
492
  Compiled methods live on the schema object, so Zod's functional API (`z.safeParse(Schema, x)`) and a
@@ -525,9 +543,9 @@ npx zod-compiler check src/schemas.ts --json --fail-under 80
525
543
 
526
544
  ## What Gets Compiled
527
545
 
528
- ### Fully Compiled (1.1-46x faster)
546
+ ### Fully Compiled (up to 42x faster)
529
547
 
530
- Every Zod type except the fallbacks below all primitives, `object` / `strictObject` / `looseObject`,
548
+ Every Zod type except the fallbacks below: all primitives, `object` / `strictObject` / `looseObject`,
531
549
  `array`, `tuple`, `record`, `set`, `map`, `union`, `discriminatedUnion`, `intersection`, `pipe`,
532
550
  the `optional` / `nullable` / `readonly` / `default` / `catch` / `coerce` wrappers, `templateLiteral`,
533
551
  recursive `lazy` (self, mutual and nested), `custom` / `instanceof`, and
@@ -550,7 +568,7 @@ A schema delegates to Zod when it reaches JavaScript the generated code cannot r
550
568
  | Dynamic error maps, unresolvable `z.lazy()` | Not knowable at build time |
551
569
 
552
570
  Everything else compiles, including context-free `preprocess` callbacks and
553
- `transform`/`refine`/`superRefine` whether or not the callback captures a zero-capture one is inlined,
571
+ `transform`/`refine`/`superRefine` whether or not the callback captures. A zero-capture one is inlined,
554
572
  a capturing one called by reference. Delegation is per-sub-schema: one uncompilable field goes to Zod,
555
573
  not the whole object. Run `zod-compiler check` to see what compiled.
556
574
 
@@ -576,55 +594,55 @@ Schema-level `error` and `z.config()` maps are unaffected; for a per-call map us
576
594
 
577
595
  | Scenario | Zod v3 | Zod v4 | **zod-compiler** | Typia | AJV | vs Zod v4 |
578
596
  | ----------------------------------------------- | ------ | ------ | ---------------- | ----- | ----- | --------- |
579
- | simple string | 8.5M | 10.4M | **11.0M** | 11.1M | 11.2M | 1.1x |
580
- | string (min/max) | 8.2M | 4.9M | **10.8M** | 11.0M | 10.0M | 2.2x |
581
- | number (int+positive) | 7.9M | 6.7M | **10.7M** | 11.1M | 11.4M | 1.6x |
582
- | enum | 8.0M | 9.4M | **11.1M** | 11.2M | 11.3M | 1.2x |
583
- | bigint (min/max) | 7.6M | 5.6M | **10.9M** | — | — | 2.0x |
584
- | tuple [string, int, bool] | 3.9M | 4.8M | **10.9M** | 10.6M | 10.1M | 2.3x |
585
- | record\<string, number\> | 2.2M | 1.8M | **8.0M** | 7.3M | 9.6M | 4.6x |
586
- | set\<string\> (5 items) | 2.5M | 1.5M | **9.9M** | — | — | 6.8x |
587
- | set\<string\> (20 items) | 918K | 445K | **7.7M** | — | — | **17x** |
588
- | map\<string, number\> (5 entries) | 1.4M | 864K | **8.7M** | — | — | **10x** |
589
- | map\<string, number\> (20 entries) | 448K | 236K | **5.5M** | — | — | **24x** |
590
- | pipe (non-transform) | 5.9M | 3.2M | **10.5M** | — | — | 3.3x |
591
- | discriminatedUnion (3 variants) | 2.3M | 3.6M | **10.7M** | 10.3M | 5.4M | 3.0x |
592
- | discriminatedUnion (8 variants, rotating) | 1.8M | 3.1M | **6.6M** | — | — | 2.1x |
593
- | plain union of 8 tagged objects (auto-discrim.) | 244K | 898K | **6.1M** | — | — | 6.8x |
594
- | strict object (DB row) | 1.2M | 2.0M | **7.0M** | — | — | 3.6x |
595
- | medium object (valid) | 1.3M | 1.5M | **5.6M** | 6.9M | 4.7M | 3.9x |
596
- | medium object (extra keys stripped) | 1.2M | 1.3M | **5.7M** | — | — | 4.3x |
597
- | medium object (invalid) | 359K | 263K | **9.8M** | 2.0M | 5.1M | **37x** |
598
- | large object (10 items) | 82K | 110K | **3.6M** | 3.8M | 765K | **33x** |
599
- | large object (100 items) | 9K | 12K | **531K** | 759K | 82K | **46x** |
600
- | readonly field (wrapper compiles away) | 2.2M | 4.7M | **11.0M** | — | — | 2.3x |
601
- | readonly root object (rebuild + freeze) | 2.1M | 4.0M | **8.6M** | — | — | 2.1x |
602
- | readonly array (delegates to Zod) | 2.8M | 3.0M | **2.9M** | — | — | 1.0x |
603
- | recursive tree (7 nodes) | 407K | 727K | **5.3M** | 7.4M | 2.9M | 7.3x |
604
- | recursive tree (121 nodes) | 23K | 42K | **510K** | 1.3M | 232K | **12x** |
605
- | nested recursion (7 nodes) | 280K | 489K | **4.6M** | 7.2M | 1.8M | 9.3x |
606
- | nested recursion (121 nodes) | 17K | 30K | **461K** | 1.1M | 126K | **15x** |
607
- | deeply nested object (243 leaves) | 8K | 20K | **327K** | 666K | 83K | **17x** |
608
- | event log (combined) | 257K | 575K | **4.9M** | — | — | 8.6x |
609
- | object with transform (zero-capture) | 752K | 1.3M | **4.4M** | — | — | 3.4x |
610
- | array 10 × transform (zero-capture) | 89K | 137K | **2.7M** | — | — | **20x** |
611
- | array 50 × transform (zero-capture) | 18K | 28K | **689K** | — | — | **25x** |
612
- | object with captured transform | 862K | 5.4M | **9.6M** | — | — | 1.8x |
613
- | object with captured refine (cross-field) | 995K | 1.3M | **7.3M** | — | — | 5.6x |
614
- | object with superRefine (cross-field) | 1.0M | 1.4M | **5.9M** | — | — | 4.3x |
615
- | coerced query object (valid) | 1.2M | 2.0M | **3.6M** | — | — | 1.8x |
616
- | coerced query object (invalid) | 754K | 603K | **6.8M** | — | — | **11x** |
617
- | preprocessed query object (valid) | 302K | 1.1M | **3.6M** | — | — | 3.2x |
618
- | preprocessed query object (invalid) | 268K | 574K | **7.8M** | — | — | **14x** |
619
- | stringbool config object (valid) | — | 2.0M | **3.7M** | — | — | 1.9x |
620
- | stringbool config object (invalid) | — | 497K | **8.6M** | — | — | **17x** |
621
- | custom/instanceof request (valid) | 689K | 2.0M | **5.6M** | — | — | 2.7x |
622
- | custom/instanceof request (invalid) | 527K | 624K | **5.7M** | — | — | 9.1x |
623
- | disjoint object intersection (valid) | 946K | 1.0M | **5.7M** | — | — | 5.5x |
624
- | disjoint object intersection (invalid) | 343K | 240K | **9.8M** | — | — | **41x** |
625
-
626
- _ops/s, higher is better. `vp test bench` on an Apple M1 Max (zod 4.5.2, zod v3 3.23.8, typia 12, ajv 8),
627
- best of three runs. The harness costs ~90 ns per iteration, so the fastest rows sit at that floor and gaps
597
+ | simple string | 12.7M | 15.8M | **16.6M** | 17.4M | 17.4M | 1.0x |
598
+ | string (min/max) | 11.9M | 7.4M | **17.5M** | 17.3M | 15.6M | 2.4x |
599
+ | number (int+positive) | 12.0M | 9.6M | **16.9M** | 16.9M | 16.9M | 1.8x |
600
+ | enum | 11.2M | 15.4M | **17.4M** | 16.9M | 17.0M | 1.1x |
601
+ | bigint (min/max) | 11.8M | 8.3M | **17.0M** | — | — | 2.1x |
602
+ | tuple [string, int, bool] | 5.7M | 7.6M | **17.9M** | 16.8M | 16.0M | 2.4x |
603
+ | record\<string, number\> | 3.1M | 2.5M | **12.6M** | 12.0M | 15.3M | 5.0x |
604
+ | set\<string\> (5 items) | 3.6M | 2.2M | **16.0M** | — | — | 7.2x |
605
+ | set\<string\> (20 items) | 1.3M | 661K | **12.4M** | — | — | **19x** |
606
+ | map\<string, number\> (5 entries) | 2.0M | 1.3M | **13.6M** | — | — | **11x** |
607
+ | map\<string, number\> (20 entries) | 618K | 338K | **8.7M** | — | — | **26x** |
608
+ | pipe (non-transform) | 8.9M | 4.7M | **17.3M** | — | — | 3.7x |
609
+ | discriminatedUnion (3 variants) | 3.3M | 5.2M | **16.2M** | 15.9M | 7.5M | 3.1x |
610
+ | discriminatedUnion (8 variants, rotating) | 2.6M | 4.4M | **9.5M** | — | — | 2.2x |
611
+ | plain union of 8 tagged objects (auto-discrim.) | 348K | 1.3M | **10.2M** | — | — | 8.1x |
612
+ | strict object (DB row) | 1.7M | 3.0M | **11.1M** | — | — | 3.6x |
613
+ | medium object (valid) | 1.9M | 2.4M | **9.9M** | 10.8M | 7.5M | 4.2x |
614
+ | medium object (extra keys stripped) | 1.8M | 2.1M | **9.8M** | — | — | 4.7x |
615
+ | medium object (invalid) | 511K | 359K | **15.7M** | 2.9M | 7.5M | **44x** |
616
+ | large object (10 items) | 117K | 175K | **5.3M** | 5.7M | 1.2M | **30x** |
617
+ | large object (100 items) | 13K | 18K | **779K** | 1.3M | 127K | **42x** |
618
+ | readonly field (wrapper compiles away) | 3.0M | 6.6M | **15.7M** | — | — | 2.4x |
619
+ | readonly root object (rebuild + freeze) | 2.8M | 5.4M | **12.8M** | — | — | 2.4x |
620
+ | readonly array (delegates to Zod) | 3.8M | 4.2M | **4.1M** | — | — | 1.0x |
621
+ | recursive tree (7 nodes) | 564K | 994K | **7.9M** | 12.1M | 4.7M | 7.9x |
622
+ | recursive tree (121 nodes) | 31K | 55K | **783K** | 1.9M | 361K | **14x** |
623
+ | nested recursion (7 nodes) | 383K | 667K | **7.9M** | 11.2M | 2.9M | **12x** |
624
+ | nested recursion (121 nodes) | 23K | 41K | **817K** | 1.6M | 206K | **20x** |
625
+ | deeply nested object (243 leaves) | 11K | 27K | **803K** | 1.0M | 122K | **30x** |
626
+ | event log (combined) | 371K | 782K | **7.5M** | — | — | 9.6x |
627
+ | object with transform (zero-capture) | 1.1M | 2.0M | **7.2M** | — | — | 3.7x |
628
+ | array 10 × transform (zero-capture) | 122K | 203K | **4.2M** | — | — | **21x** |
629
+ | array 50 × transform (zero-capture) | 25K | 42K | **1.0M** | — | — | **25x** |
630
+ | object with captured transform | 1.2M | 8.2M | **16.2M** | — | — | 2.0x |
631
+ | object with captured refine (cross-field) | 1.4M | 2.2M | **11.2M** | — | — | 5.0x |
632
+ | object with superRefine (cross-field) | 1.4M | 2.2M | **9.3M** | — | — | 4.3x |
633
+ | coerced query object (valid) | 1.8M | 2.9M | **5.4M** | — | — | 1.9x |
634
+ | coerced query object (invalid) | 1.0M | 828K | **10.3M** | — | — | **12x** |
635
+ | preprocessed query object (valid) | 401K | 1.7M | **5.2M** | — | — | 3.0x |
636
+ | preprocessed query object (invalid) | 378K | 755K | **12.7M** | — | — | **17x** |
637
+ | stringbool config object (valid) | — | 2.8M | **6.3M** | — | — | 2.2x |
638
+ | stringbool config object (invalid) | — | 670K | **13.8M** | — | — | **21x** |
639
+ | custom/instanceof request (valid) | 965K | 3.1M | **10.4M** | — | — | 3.3x |
640
+ | custom/instanceof request (invalid) | 784K | 930K | **10.2M** | — | — | **11x** |
641
+ | disjoint object intersection (valid) | 1.4M | 1.6M | **9.8M** | — | — | 6.0x |
642
+ | disjoint object intersection (invalid) | 493K | 327K | **15.4M** | — | — | **47x** |
643
+
644
+ _ops/s, higher is better. `vp test bench` on an Apple M4 Max (zod 4.5.2, zod v3 3.23.8, typia 12, ajv 8),
645
+ best of three runs. The harness costs ~60 ns per iteration, so the fastest rows sit at that floor and gaps
628
646
  between the AOT columns there are noise, not real._
629
647
 
630
648
  Nested objects, arrays and recursive types gain the most. Rejection is fast because a failed
@@ -638,23 +656,27 @@ vp run benchmark # run locally
638
656
 
639
657
  ### Performance Architecture
640
658
 
641
- An eligible schema compiles to a **fast path** one `&&` chain validating the whole input with zero
642
- allocations, reused by `.is()` and `parse()` plus a **slow path** that collects errors, run only on
643
- failure and deferred until `.error` is read. A `z.object()` strips, so it instead compiles to a single
644
- pass that validates and rebuilds together, bailing on the first failure including the reshaping
645
- idioms (array size checks, `.refine()`, `.default()`, `.trim()`, `.transform()`).
659
+ An eligible schema compiles to a **fast path**, one `&&` chain validating the whole input with zero
660
+ allocations and reused by `.is()` and `parse()`, plus a **slow path** that collects errors, runs only on
661
+ failure, and is deferred until `.error` is read. A `z.object()` strips, so it instead compiles to a
662
+ single pass that validates and rebuilds together and bails on the first failure, covering the reshaping
663
+ idioms too (array size checks, `.refine()`, `.default()`, `.trim()`, `.transform()`).
646
664
 
647
665
  Regexes are pre-compiled with bounded repeats unrolled, checks run cheapest-first, discriminated unions
648
- dispatch through a jump table (plain tagged unions are auto-discriminated into it), and oversized check
649
- functions are split to stay within V8's optimizer budget. Stripping objects, native coercions,
666
+ dispatch through a `switch` on the tag (plain tagged unions are auto-discriminated into it, on the
667
+ stripping pass as well as the fast check), and oversized check functions are split to stay within V8's
668
+ optimizer budget. `z.email()` runs as a single linear scan instead of a backtracking regex, a record's
669
+ plain-object guard exits on one comparison for an ordinary object, and a case-insensitive `stringbool`
670
+ looks its input up verbatim before paying for `toLowerCase()`. Stripping objects, native coercions,
650
671
  `stringbool`, defaults, string rewrites, context-free preprocessors and synchronous transforms validate
651
672
  and build their output in one pass. An intersection of two objects with disjoint keys compiles to that
652
673
  same single pass over the merged shape, and `z.custom()` / `z.instanceof()` compile to a direct predicate
653
674
  call.
654
675
 
655
- Where success is cheaper to compile than failure, only the verdict and output are compiled: intersections
656
- and `custom` keep the original Zod schema to construct issues, so a rejection still reports exactly what
657
- Zod would — including an intersection's one-issue-per-side shape without slowing the hot path.
676
+ Where success is cheaper to compile than failure, only the verdict and output are compiled.
677
+ Intersections and `custom` keep the original Zod schema to construct issues, so a rejection still
678
+ reports exactly what Zod would, an intersection's one-issue-per-side shape included, without slowing the
679
+ hot path.
658
680
 
659
681
  ## Development
660
682
 
@@ -1 +1 @@
1
- {"version":3,"file":"build-path.d.ts","names":[],"sources":["../../../src/core/codegen/build-path.ts"],"mappings":";;;;iBA8KgB,eAAe,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiCnB,kBAAkB,IAAI;;;;;;;;;;;iBAmFtB,sBAAsB,IAAI;;;;;;iBAmD1B,cAAc,IAAI,UAAU,KAAK"}
1
+ {"version":3,"file":"build-path.d.ts","names":[],"sources":["../../../src/core/codegen/build-path.ts"],"mappings":";;;;iBAyLgB,eAAe,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiCnB,kBAAkB,IAAI;;;;;;;;;;;iBAmFtB,sBAAsB,IAAI;;;;;;iBAmD1B,cAAc,IAAI,UAAU,KAAK"}