zod-compiler 2.0.0 → 2.0.2
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 +175 -151
- package/dist/core/codegen/build-path.d.ts.map +1 -1
- package/dist/core/codegen/build-path.js +144 -34
- package/dist/core/codegen/build-path.js.map +1 -1
- package/dist/core/codegen/context.d.ts +11 -10
- package/dist/core/codegen/context.d.ts.map +1 -1
- package/dist/core/codegen/context.js.map +1 -1
- package/dist/core/codegen/fast-path.d.ts +3 -1
- package/dist/core/codegen/fast-path.d.ts.map +1 -1
- package/dist/core/codegen/fast-path.js +7 -4
- package/dist/core/codegen/fast-path.js.map +1 -1
- package/dist/core/codegen/index.js +4 -3
- package/dist/core/codegen/index.js.map +1 -1
- package/dist/core/codegen/issue-decls.d.ts +43 -1
- package/dist/core/codegen/issue-decls.d.ts.map +1 -1
- package/dist/core/codegen/issue-decls.js +45 -2
- package/dist/core/codegen/issue-decls.js.map +1 -1
- package/dist/core/codegen/schemas/default.d.ts +10 -0
- package/dist/core/codegen/schemas/default.d.ts.map +1 -1
- package/dist/core/codegen/schemas/default.js +11 -0
- package/dist/core/codegen/schemas/default.js.map +1 -1
- package/dist/core/codegen/schemas/discriminated-union.d.ts +6 -2
- package/dist/core/codegen/schemas/discriminated-union.d.ts.map +1 -1
- package/dist/core/codegen/schemas/discriminated-union.js +14 -55
- package/dist/core/codegen/schemas/discriminated-union.js.map +1 -1
- package/dist/core/codegen/schemas/string-bool.d.ts +12 -1
- package/dist/core/codegen/schemas/string-bool.d.ts.map +1 -1
- package/dist/core/codegen/schemas/string-bool.js +19 -4
- package/dist/core/codegen/schemas/string-bool.js.map +1 -1
- package/dist/core/codegen/schemas/string.d.ts.map +1 -1
- package/dist/core/codegen/schemas/string.js +15 -10
- package/dist/core/codegen/schemas/string.js.map +1 -1
- package/dist/core/codegen/well-known-regex.d.ts +19 -1
- package/dist/core/codegen/well-known-regex.d.ts.map +1 -1
- package/dist/core/codegen/well-known-regex.js +21 -1
- package/dist/core/codegen/well-known-regex.js.map +1 -1
- package/dist/core/iife.d.ts +8 -0
- package/dist/core/iife.d.ts.map +1 -1
- package/dist/core/iife.js +8 -0
- package/dist/core/iife.js.map +1 -1
- package/dist/runtime.d.ts +1 -0
- package/dist/runtime.js +2 -1
- package/package.json +1 -1
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 **
|
|
5
|
+
Keep your existing Zod schemas. Get **up to 44x faster** validation, and up to **46x** on rejected
|
|
6
6
|
input. No code changes required.
|
|
7
7
|
|
|
8
|
-
Requires **Zod ≥ 4.5**.
|
|
9
|
-
lengths, symbol-keyed shapes
|
|
10
|
-
zod-compiler 1.x
|
|
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 44x; up to 46x 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
116
|
-
|
|
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
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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"`
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
180
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
>
|
|
217
|
-
>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
254
|
-
|
|
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
|
|
260
|
-
**19-28% raw / 10-18% gzipped
|
|
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
|
|
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
|
|
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 } }]
|
|
309
|
-
|
|
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"
|
|
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
|
-
[
|
|
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
|
|
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
|
-
|
|
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
|
|
357
|
-
|
|
358
|
-
|
|
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**
|
|
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
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
381
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
|
435
|
-
12 performance cores
|
|
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
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
|
448
|
-
cache key, so
|
|
449
|
-
|
|
450
|
-
|
|
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
|
|
453
|
-
|
|
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
|
|
458
|
-
|
|
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
|
|
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
|
|
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 (
|
|
546
|
+
### Fully Compiled (up to 44x faster)
|
|
529
547
|
|
|
530
|
-
Every Zod type except the fallbacks below
|
|
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
|
|
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 |
|
|
580
|
-
| string (min/max) |
|
|
581
|
-
| number (int+positive) |
|
|
582
|
-
| enum |
|
|
583
|
-
| bigint (min/max) |
|
|
584
|
-
| tuple [string, int, bool] |
|
|
585
|
-
| record\<string, number\> |
|
|
586
|
-
| set\<string\> (5 items) |
|
|
587
|
-
| set\<string\> (20 items) |
|
|
588
|
-
| map\<string, number\> (5 entries) |
|
|
589
|
-
| map\<string, number\> (20 entries) |
|
|
590
|
-
| pipe (non-transform) |
|
|
591
|
-
| discriminatedUnion (3 variants) |
|
|
592
|
-
| discriminatedUnion (8 variants, rotating) |
|
|
593
|
-
| plain union of 8 tagged objects (auto-discrim.) |
|
|
594
|
-
| strict object (DB row) | 1.
|
|
595
|
-
| medium object (valid) | 1.
|
|
596
|
-
| medium object (extra keys stripped) | 1.
|
|
597
|
-
| medium object (invalid) |
|
|
598
|
-
| large object (10 items) |
|
|
599
|
-
| large object (100 items) |
|
|
600
|
-
| readonly field (wrapper compiles away) |
|
|
601
|
-
| readonly root object (rebuild + freeze) | 2.
|
|
602
|
-
| readonly array (delegates to Zod) |
|
|
603
|
-
| recursive tree (7 nodes) |
|
|
604
|
-
| recursive tree (121 nodes) |
|
|
605
|
-
| nested recursion (7 nodes) |
|
|
606
|
-
| nested recursion (121 nodes) |
|
|
607
|
-
| deeply nested object (243 leaves) |
|
|
608
|
-
| event log (combined) |
|
|
609
|
-
| object with transform (zero-capture) |
|
|
610
|
-
| array 10 × transform (zero-capture) |
|
|
611
|
-
| array 50 × transform (zero-capture) |
|
|
612
|
-
| object with captured transform |
|
|
613
|
-
| object with captured refine (cross-field) |
|
|
614
|
-
| object with superRefine (cross-field) | 1.
|
|
615
|
-
| coerced query object (valid) | 1.
|
|
616
|
-
| coerced query object (invalid) |
|
|
617
|
-
| preprocessed query object (valid) |
|
|
618
|
-
| preprocessed query object (invalid) |
|
|
619
|
-
| stringbool config object (valid) | — | 2.
|
|
620
|
-
| stringbool config object (invalid) | — |
|
|
621
|
-
| custom/instanceof request (valid) |
|
|
622
|
-
| custom/instanceof request (invalid) |
|
|
623
|
-
| disjoint object intersection (valid) |
|
|
624
|
-
| disjoint object intersection (invalid) |
|
|
625
|
-
|
|
626
|
-
_ops/s, higher is better. `vp test bench` on an Apple
|
|
627
|
-
best of three runs. The harness costs ~
|
|
597
|
+
| simple string | 12.6M | 15.6M | **16.8M** | 17.2M | 17.4M | 1.1x |
|
|
598
|
+
| string (min/max) | 12.4M | 7.4M | **17.4M** | 17.6M | 15.5M | 2.4x |
|
|
599
|
+
| number (int+positive) | 11.8M | 9.4M | **17.2M** | 16.3M | 17.4M | 1.8x |
|
|
600
|
+
| enum | 12.1M | 14.7M | **17.4M** | 17.4M | 17.2M | 1.2x |
|
|
601
|
+
| bigint (min/max) | 11.2M | 7.9M | **16.7M** | — | — | 2.1x |
|
|
602
|
+
| tuple [string, int, bool] | 5.6M | 7.3M | **17.4M** | 16.4M | 15.7M | 2.4x |
|
|
603
|
+
| record\<string, number\> | 3.1M | 2.6M | **12.7M** | 12.0M | 15.2M | 5.0x |
|
|
604
|
+
| set\<string\> (5 items) | 3.7M | 2.3M | **15.2M** | — | — | 6.6x |
|
|
605
|
+
| set\<string\> (20 items) | 1.3M | 680K | **12.1M** | — | — | **18x** |
|
|
606
|
+
| map\<string, number\> (5 entries) | 2.0M | 1.3M | **13.2M** | — | — | **10x** |
|
|
607
|
+
| map\<string, number\> (20 entries) | 637K | 347K | **8.5M** | — | — | **24x** |
|
|
608
|
+
| pipe (non-transform) | 8.7M | 4.7M | **17.3M** | — | — | 3.6x |
|
|
609
|
+
| discriminatedUnion (3 variants) | 3.3M | 5.2M | **17.2M** | 16.1M | 7.8M | 3.3x |
|
|
610
|
+
| discriminatedUnion (8 variants, rotating) | 2.6M | 4.4M | **10.3M** | — | — | 2.3x |
|
|
611
|
+
| plain union of 8 tagged objects (auto-discrim.) | 356K | 1.3M | **10.2M** | — | — | 7.7x |
|
|
612
|
+
| strict object (DB row) | 1.8M | 3.0M | **11.3M** | — | — | 3.8x |
|
|
613
|
+
| medium object (valid) | 1.9M | 2.2M | **9.4M** | 11.2M | 7.5M | 4.2x |
|
|
614
|
+
| medium object (extra keys stripped) | 1.8M | 2.0M | **9.8M** | — | — | 4.9x |
|
|
615
|
+
| medium object (invalid) | 534K | 372K | **15.5M** | 2.9M | 7.5M | **42x** |
|
|
616
|
+
| large object (10 items) | 120K | 163K | **5.1M** | 5.9M | 1.2M | **31x** |
|
|
617
|
+
| large object (100 items) | 13K | 17K | **766K** | 1.3M | 127K | **44x** |
|
|
618
|
+
| readonly field (wrapper compiles away) | 3.0M | 6.6M | **16.7M** | — | — | 2.5x |
|
|
619
|
+
| readonly root object (rebuild + freeze) | 2.8M | 5.5M | **12.9M** | — | — | 2.3x |
|
|
620
|
+
| readonly array (delegates to Zod) | 3.9M | 4.3M | **4.2M** | — | — | 1.0x |
|
|
621
|
+
| recursive tree (7 nodes) | 581K | 1.0M | **7.5M** | 11.6M | 4.7M | 7.4x |
|
|
622
|
+
| recursive tree (121 nodes) | 31K | 57K | **777K** | 1.9M | 371K | **14x** |
|
|
623
|
+
| nested recursion (7 nodes) | 395K | 683K | **7.9M** | 11.2M | 3.0M | **12x** |
|
|
624
|
+
| nested recursion (121 nodes) | 24K | 42K | **805K** | 1.6M | 204K | **19x** |
|
|
625
|
+
| deeply nested object (243 leaves) | 11K | 27K | **825K** | 1.0M | 125K | **30x** |
|
|
626
|
+
| event log (combined) | 363K | 795K | **7.3M** | — | — | 9.2x |
|
|
627
|
+
| object with transform (zero-capture) | 1.1M | 1.9M | **6.7M** | — | — | 3.5x |
|
|
628
|
+
| array 10 × transform (zero-capture) | 121K | 206K | **4.1M** | — | — | **20x** |
|
|
629
|
+
| array 50 × transform (zero-capture) | 26K | 41K | **1.0M** | — | — | **25x** |
|
|
630
|
+
| object with captured transform | 1.2M | 7.9M | **15.9M** | — | — | 2.0x |
|
|
631
|
+
| object with captured refine (cross-field) | 1.4M | 2.2M | **11.5M** | — | — | 5.2x |
|
|
632
|
+
| object with superRefine (cross-field) | 1.4M | 2.1M | **9.2M** | — | — | 4.3x |
|
|
633
|
+
| coerced query object (valid) | 1.8M | 2.9M | **5.3M** | — | — | 1.9x |
|
|
634
|
+
| coerced query object (invalid) | 1.0M | 826K | **10.3M** | — | — | **12x** |
|
|
635
|
+
| preprocessed query object (valid) | 433K | 1.7M | **5.3M** | — | — | 3.2x |
|
|
636
|
+
| preprocessed query object (invalid) | 392K | 782K | **12.3M** | — | — | **16x** |
|
|
637
|
+
| stringbool config object (valid) | — | 2.8M | **6.3M** | — | — | 2.2x |
|
|
638
|
+
| stringbool config object (invalid) | — | 682K | **13.2M** | — | — | **19x** |
|
|
639
|
+
| custom/instanceof request (valid) | 965K | 3.1M | **9.9M** | — | — | 3.2x |
|
|
640
|
+
| custom/instanceof request (invalid) | 786K | 928K | **13.2M** | — | — | **14x** |
|
|
641
|
+
| disjoint object intersection (valid) | 1.4M | 1.6M | **9.5M** | — | — | 5.9x |
|
|
642
|
+
| disjoint object intersection (invalid) | 488K | 331K | **15.3M** | — | — | **46x** |
|
|
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,29 @@ vp run benchmark # run locally
|
|
|
638
656
|
|
|
639
657
|
### Performance Architecture
|
|
640
658
|
|
|
641
|
-
An eligible schema compiles to a **fast path
|
|
642
|
-
allocations
|
|
643
|
-
failure and deferred until `.error` is read. A `z.object()` strips, so it instead compiles to a
|
|
644
|
-
pass that validates and rebuilds together
|
|
645
|
-
idioms (array size checks, `.refine()`, `.default()`, `.trim()`, `.transform()`).
|
|
646
|
-
|
|
647
|
-
Regexes are pre-compiled with bounded repeats unrolled, checks run cheapest-first
|
|
648
|
-
|
|
649
|
-
|
|
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()`).
|
|
664
|
+
|
|
665
|
+
Regexes are pre-compiled with bounded repeats unrolled, checks run cheapest-first on both passes (so a
|
|
666
|
+
payload with a wrong boolean is rejected before its email is scanned, whichever was declared first),
|
|
667
|
+
discriminated unions dispatch through a `switch` on the tag (plain tagged unions are auto-discriminated
|
|
668
|
+
into it, on the stripping pass as well as the fast check, and the selected option re-checks neither
|
|
669
|
+
object-ness nor the tag), and oversized check functions are split to stay within V8's optimizer budget.
|
|
670
|
+
`.is()` stays a zero-allocation predicate on schemas with `.default()` fields. `z.email()` runs as a single linear scan instead of a backtracking regex, a record's
|
|
671
|
+
plain-object guard exits on one comparison for an ordinary object, and a case-insensitive `stringbool`
|
|
672
|
+
looks its input up verbatim before paying for `toLowerCase()`. Stripping objects, native coercions,
|
|
650
673
|
`stringbool`, defaults, string rewrites, context-free preprocessors and synchronous transforms validate
|
|
651
674
|
and build their output in one pass. An intersection of two objects with disjoint keys compiles to that
|
|
652
675
|
same single pass over the merged shape, and `z.custom()` / `z.instanceof()` compile to a direct predicate
|
|
653
676
|
call.
|
|
654
677
|
|
|
655
|
-
Where success is cheaper to compile than failure, only the verdict and output are compiled
|
|
656
|
-
and `custom` keep the original Zod schema to construct issues, so a rejection still
|
|
657
|
-
Zod would
|
|
678
|
+
Where success is cheaper to compile than failure, only the verdict and output are compiled.
|
|
679
|
+
Intersections and `custom` keep the original Zod schema to construct issues, so a rejection still
|
|
680
|
+
reports exactly what Zod would, an intersection's one-issue-per-side shape included, without slowing the
|
|
681
|
+
hot path.
|
|
658
682
|
|
|
659
683
|
## Development
|
|
660
684
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-path.d.ts","names":[],"sources":["../../../src/core/codegen/build-path.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"build-path.d.ts","names":[],"sources":["../../../src/core/codegen/build-path.ts"],"mappings":";;;;iBAuMgB,eAAe,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiCnB,kBAAkB,IAAI;;;;;;;;;;;iBAmFtB,sBAAsB,IAAI;;;;;;iBAmD1B,cAAc,IAAI,UAAU,KAAK"}
|