zod-compiler 2.0.5 → 2.0.7

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.
Files changed (48) hide show
  1. package/README.md +97 -118
  2. package/dist/core/codegen/build-path.d.ts.map +1 -1
  3. package/dist/core/codegen/build-path.js +16 -7
  4. package/dist/core/codegen/build-path.js.map +1 -1
  5. package/dist/core/codegen/context.d.ts +10 -7
  6. package/dist/core/codegen/context.d.ts.map +1 -1
  7. package/dist/core/codegen/context.js +10 -8
  8. package/dist/core/codegen/context.js.map +1 -1
  9. package/dist/core/codegen/index.js +3 -2
  10. package/dist/core/codegen/index.js.map +1 -1
  11. package/dist/core/codegen/issue-decls.d.ts +28 -1
  12. package/dist/core/codegen/issue-decls.d.ts.map +1 -1
  13. package/dist/core/codegen/issue-decls.js +30 -1
  14. package/dist/core/codegen/issue-decls.js.map +1 -1
  15. package/dist/core/codegen/schemas/array.js +2 -2
  16. package/dist/core/codegen/schemas/array.js.map +1 -1
  17. package/dist/core/codegen/schemas/custom.js +1 -1
  18. package/dist/core/codegen/schemas/discriminated-union.js +1 -1
  19. package/dist/core/codegen/schemas/discriminated-union.js.map +1 -1
  20. package/dist/core/codegen/schemas/effect.js +2 -2
  21. package/dist/core/codegen/schemas/effect.js.map +1 -1
  22. package/dist/core/codegen/schemas/fallback.js +1 -1
  23. package/dist/core/codegen/schemas/map.js +1 -1
  24. package/dist/core/codegen/schemas/number.js +1 -1
  25. package/dist/core/codegen/schemas/object.js +3 -3
  26. package/dist/core/codegen/schemas/object.js.map +1 -1
  27. package/dist/core/codegen/schemas/record.js +2 -2
  28. package/dist/core/codegen/schemas/record.js.map +1 -1
  29. package/dist/core/codegen/schemas/sizeable.js +1 -1
  30. package/dist/core/codegen/schemas/string.d.ts +15 -2
  31. package/dist/core/codegen/schemas/string.d.ts.map +1 -1
  32. package/dist/core/codegen/schemas/string.js +97 -35
  33. package/dist/core/codegen/schemas/string.js.map +1 -1
  34. package/dist/core/codegen/schemas/tuple.js +2 -2
  35. package/dist/core/codegen/schemas/tuple.js.map +1 -1
  36. package/dist/core/codegen/schemas/union.js +1 -1
  37. package/dist/core/iife.d.ts +11 -12
  38. package/dist/core/iife.d.ts.map +1 -1
  39. package/dist/core/iife.js +12 -13
  40. package/dist/core/iife.js.map +1 -1
  41. package/dist/runtime.d.ts +2 -0
  42. package/dist/runtime.js +3 -1
  43. package/dist/static-filter.d.ts +19 -1
  44. package/dist/static-filter.d.ts.map +1 -1
  45. package/dist/static-filter.js +27 -1
  46. package/dist/static-filter.js.map +1 -1
  47. package/dist/unplugin/virtual.js +1 -1
  48. package/package.json +1 -1
package/README.md CHANGED
@@ -2,14 +2,12 @@
2
2
 
3
3
  **Compile Zod schemas into zero-overhead validation functions at build time.**
4
4
 
5
- Keep your existing Zod schemas. Get **up to 44x faster** validation, and up to **46x** on rejected
5
+ Keep your existing Zod schemas. Get **up to 43x faster** validation, and up to **140x** on rejected
6
6
  input. No code changes required.
7
7
 
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. An older Zod is refused with an explicit error (the build plugin
11
- and `jit()` warn once and leave the schemas as plain Zod) rather than compiled into validators that
12
- disagree with it.
8
+ Requires **Zod ≥ 4.5**; compiled output reproduces 4.5's semantics exactly, so it does not match
9
+ earlier 4.x releases. Use zod-compiler 1.x for Zod 4.0–4.4. An older Zod is refused with an explicit
10
+ error; the build plugin and `jit()` warn once and leave schemas as plain Zod.
13
11
 
14
12
  - [What Gets Compiled](#what-gets-compiled)
15
13
  - [Schema Hoisting](#schema-hoisting)
@@ -28,16 +26,12 @@ loads pre-generated validators.
28
26
  | | zod-compiler (build plugins / CLI) | Zod `z.compile()` |
29
27
  | -------------------------------------- | -------------------------------------------- | --------------------------------------------- |
30
28
  | Compilation | Build time (true AOT) | Runtime (`z.compile()` or the first parse) |
31
- | Reported validation speedup | Up to 44x; up to 46x on rejected input | ~9x in Zod's headline example |
32
- | Uses `new Function()` at runtime\* | No | Yes |
29
+ | Reported validation speedup | Up to 43x; up to 140x on rejected input | ~9x in Zod's headline example |
30
+ | Uses `new Function()` at runtime | No | Yes |
33
31
  | Cold start | Fast; the validator is already generated | Pays for code generation at startup/first use |
34
32
  | Strict CSP without `'unsafe-eval'` | Supported | Compilation is unavailable |
35
33
  | Compiler shipped in the runtime bundle | No; only validators and runtime helpers ship | Yes; about 7 KB gzipped according to Zod |
36
34
 
37
- \*zod-compiler's optional [`jit()`](#4-runtime-compilation-no-build-step) and
38
- [Node.js register hook](#5-nodejs-register-hook) use `new Function()` and have the same runtime
39
- code-generation and CSP trade-offs as Zod's `z.compile()`.
40
-
41
35
  ## Usage
42
36
 
43
37
  Five ways to use zod-compiler. Pick one:
@@ -135,8 +129,7 @@ Compilation is lazy, costing 0.1-0.3 ms on a schema's first parse. `{ eager: tru
135
129
  and `jitAll(namespace)` takes a whole module.
136
130
 
137
131
  The cost is the import: ~570 KB of codegen and `acorn`, **~10 ms of module load**. That suits a
138
- long-lived process, not a CLI, a cold serverless handler or a browser. Use the build plugin there, and
139
- have libraries ship plain Zod so the app can decide.
132
+ long-lived process, not a CLI, a cold serverless handler or a browser.
140
133
 
141
134
  Needs `new Function`, as Zod's own object fast-path does. `z.config({ jitless: true })` and a CSP
142
135
  that blocks eval both leave a working plain-Zod schema.
@@ -157,11 +150,9 @@ also chains with TypeScript runners:
157
150
  node --import zod-compiler/register --import tsx src/server.ts
158
151
  ```
159
152
 
160
- This is runtime JIT instrumentation, not the AOT source rewriting the build plugins perform. The hook
161
- registers the live Zod objects behind exported schema bindings and generates validators in-process on
162
- first use. It does not execute modules twice, and adds no cache beyond Node's own module cache. Use a
163
- build plugin or the CLI when validator code must exist before Node starts, or when `new Function` is
164
- unavailable at runtime.
153
+ This is runtime JIT instrumentation, not AOT source rewriting: the hook generates validators
154
+ in-process on first use. Use a build plugin or the CLI when validator code must exist before Node
155
+ starts, or when `new Function` is unavailable.
165
156
 
166
157
  Optional settings come from `zod-compiler.json` in the working directory:
167
158
 
@@ -282,10 +273,9 @@ bundle. Schemas in a file that share a structurally identical sub-shape emit its
282
273
  **19-28% raw / 10-18% gzipped** and scaling with how much the file repeats.
283
274
 
284
275
  Build plugins serve that module from a resolve hook (`virtual:zod-compiler/runtime`, or
285
- `__zod-compiler-runtime__` on webpack and rspack, which reject the `virtual:` scheme). A loader host
286
- has no hook, so [Turbopack](#nextjs-turbopack) imports the same code from the real subpath
287
- `zod-compiler/runtime` instead. It is opt-in there, since it only pays off where the host bundles that
288
- import rather than leaving it external.
276
+ `__zod-compiler-runtime__` on webpack and rspack). [Turbopack](#nextjs-turbopack) has no such hook, so
277
+ it imports the real subpath `zod-compiler/runtime` instead, opt-in because it only pays off where the
278
+ host bundles that import.
289
279
 
290
280
  **Transpile-only esbuild builds** (no `--bundle`) never fire the bundler's resolve hooks, so the
291
281
  `virtual:` specifier would survive into `dist/` and fail at runtime. Set `codegenMode: "inline"` to emit
@@ -376,8 +366,7 @@ The step pays for itself: **Hermes ships no JIT and no `new Function`**, so Zod'
376
366
  is unavailable on device and [`jit()`](#4-runtime-compilation-no-build-step) cannot run there at all.
377
367
 
378
368
  Keep schema modules free of `react-native` and `expo-*` imports, transitively. Discovery executes each
379
- file and its import graph in Node (in both modes), and one that throws falls back to runtime Zod
380
- silently.
369
+ file and its import graph in Node, and one that throws falls back to runtime Zod silently.
381
370
 
382
371
  ### Compact Output (`output: "compact"`)
383
372
 
@@ -391,14 +380,13 @@ zodCompiler({ output: "compact" });
391
380
 
392
381
  ### Workers and Serverless Startup
393
382
 
394
- Workers construct every imported schema at module init, even when an isolate validates only a few.
395
- Compiling all of them buys validation speed at the cost of bundle size and startup work. Compact output
396
- trims the generated error path but still retains the Zod schema, and does not make eager construction
397
- lazy.
383
+ Workers construct every imported schema at module init, even when an isolate validates only a few, so
384
+ compiling all of them trades bundle size and startup work for validation speed. Compact output trims
385
+ the generated error path but does not make construction lazy.
398
386
 
399
- If your app has a clear schema boundary, narrow `include` or use `schemas: "explicit"` to skip
400
- intermediate exports. Use `output: "bag"` only where consumers need no Zod APIs (`.shape`, `.extend()`,
401
- `.meta()`, `z.toJSONSchema()`); it can drop the retained schema entirely.
387
+ Narrow `include` or use `schemas: "explicit"` to skip intermediate exports. Use `output: "bag"` where
388
+ consumers need no Zod APIs (`.shape`, `.extend()`, `.meta()`, `z.toJSONSchema()`); it drops the
389
+ retained schema entirely.
402
390
 
403
391
  Measure startup separately from validation throughput, on the target deployment and bundle.
404
392
 
@@ -439,10 +427,9 @@ never mention `zod` cost nothing.
439
427
 
440
428
  ### Parallel Transforms
441
429
 
442
- Discovery runs one file at a time on the bundler's own thread, serializing executions so concurrent
443
- transforms cannot double-execute a shared dependency. `parallel` moves whole transforms onto worker
444
- threads instead, each with its own loader and module cache, which is what makes running them at the
445
- same time sound.
430
+ Discovery runs one file at a time on the bundler's thread so concurrent transforms cannot
431
+ double-execute a shared dependency. `parallel` moves whole transforms onto worker threads, each with
432
+ its own loader and module cache.
446
433
 
447
434
  ```typescript
448
435
  zodCompiler({ parallel: true }); // one worker per core, less one, capped at 4
@@ -459,18 +446,15 @@ win; files chained through each other can lose. Both rows below are 120 files of
459
446
  | independent graphs | 3,633 ms | 2,263 ms | 1,508 ms | 1,786 ms | 2,119 ms |
460
447
  | 120-deep import chain | 945 ms | 977 ms | 1,045 ms | 1,796 ms | 3,332 ms |
461
448
 
462
- So measure before adopting it. `ZOD_COMPILER_TIMING=1` prints the per-phase breakdown, and `discover`
463
- is the line workers move. Throughput peaks around four workers and declines past it: every extra worker
464
- re-executes more graph, holds another copy in memory, and adds to the generated source that the single
465
- receiving thread has to deserialize.
449
+ Measure before adopting it: `ZOD_COMPILER_TIMING=1` prints the per-phase breakdown, and `discover` is
450
+ the line workers move. Throughput peaks around four workers and declines past it, as every extra
451
+ worker re-executes more graph and holds another copy in memory.
466
452
 
467
- Emitted code, sourcemaps and cache entries are identical either way. `parallel` is not part of the
468
- cache key, so parallel and serial builds share one cache. Disk caching and dependency crawling stay on
469
- the bundler thread, and a worker that cannot start or dies mid-build has its file retried in-process
470
- rather than failing the build.
453
+ Emitted code, sourcemaps and cache entries are identical either way, and `parallel` is not part of the
454
+ cache key, so parallel and serial builds share one cache. A worker that cannot start or dies mid-build
455
+ has its file retried in-process.
471
456
 
472
- A **warm cache still beats parallelism** and costs no memory. Reach for `parallel` on the cold runs the
473
- cache cannot help with.
457
+ A **warm cache still beats parallelism** and costs no memory. Reach for `parallel` on cold runs.
474
458
 
475
459
  ## Framework Examples
476
460
 
@@ -545,7 +529,7 @@ npx zod-compiler check src/schemas.ts --json --fail-under 80
545
529
 
546
530
  ## What Gets Compiled
547
531
 
548
- ### Fully Compiled (up to 44x faster)
532
+ ### Fully Compiled (up to 43x faster)
549
533
 
550
534
  Every Zod type except the fallbacks below: all primitives, `object` / `strictObject` / `looseObject`,
551
535
  `array`, `tuple`, `record`, `set`, `map`, `union`, `discriminatedUnion`, `intersection`, `pipe`,
@@ -564,7 +548,7 @@ A schema delegates to Zod when it reaches JavaScript the generated code cannot r
564
548
  | ---------------------------------------------------- | -------------------------------------------------------------------------- |
565
549
  | `.check(fn)`, `superRefine` + later checks | The callback holds Zod's payload unmediated, or `fatal` aborts Zod's chain |
566
550
  | `ctx`-taking or `async` callbacks | Needs Zod's parse context / the async pipeline |
567
- | `z.url()`, `z.jwt()` | Algorithmic formats (`new URL()`, signature parsing) |
551
+ | `z.jwt()` | Algorithmic format (signature parsing) |
568
552
  | Overlapping or policy-sensitive object intersections | Zod's independent parse-and-merge semantics cannot be safely collapsed |
569
553
  | `.readonly()` over a pass-through container | Zod freezes the output it rebuilt; these are the caller's own input |
570
554
  | Dynamic error maps, unresolvable `z.lazy()` | Not knowable at build time |
@@ -596,61 +580,63 @@ Schema-level `error` and `z.config()` maps are unaffected; for a per-call map us
596
580
 
597
581
  | Scenario | Zod v3 | Zod v4 | **zod-compiler** | Typia | AJV | vs Zod v4 |
598
582
  | ----------------------------------------------- | ------ | ------ | ---------------- | ----- | ----- | --------- |
599
- | simple string | 12.6M | 15.6M | **16.8M** | 17.2M | 17.4M | 1.1x |
600
- | string (min/max) | 12.4M | 7.4M | **17.4M** | 17.6M | 15.5M | 2.4x |
601
- | number (int+positive) | 11.8M | 9.4M | **17.2M** | 16.3M | 17.4M | 1.8x |
602
- | enum | 12.1M | 14.7M | **17.4M** | 17.4M | 17.2M | 1.2x |
603
- | bigint (min/max) | 11.2M | 7.9M | **16.7M** | — | — | 2.1x |
604
- | tuple [string, int, bool] | 5.6M | 7.3M | **17.4M** | 16.4M | 15.7M | 2.4x |
605
- | record\<string, number\> | 3.1M | 2.6M | **12.7M** | 12.0M | 15.2M | 5.0x |
606
- | set\<string\> (5 items) | 3.7M | 2.3M | **15.2M** | — | — | 6.6x |
607
- | set\<string\> (20 items) | 1.3M | 680K | **12.1M** | — | — | **18x** |
608
- | map\<string, number\> (5 entries) | 2.0M | 1.3M | **13.2M** | — | — | **10x** |
609
- | map\<string, number\> (20 entries) | 637K | 347K | **8.5M** | — | — | **24x** |
610
- | pipe (non-transform) | 8.7M | 4.7M | **17.3M** | — | — | 3.6x |
611
- | discriminatedUnion (3 variants) | 3.3M | 5.2M | **17.2M** | 16.1M | 7.8M | 3.3x |
612
- | discriminatedUnion (8 variants, rotating) | 2.6M | 4.4M | **10.3M** | — | — | 2.3x |
613
- | plain union of 8 tagged objects (auto-discrim.) | 356K | 1.3M | **10.2M** | — | — | 7.7x |
614
- | strict object (DB row) | 1.8M | 3.0M | **11.3M** | — | — | 3.8x |
615
- | medium object (valid) | 1.9M | 2.2M | **9.4M** | 11.2M | 7.5M | 4.2x |
616
- | medium object (extra keys stripped) | 1.8M | 2.0M | **9.8M** | — | — | 4.9x |
617
- | medium object (invalid) | 534K | 372K | **15.5M** | 2.9M | 7.5M | **42x** |
618
- | large object (10 items) | 120K | 163K | **5.1M** | 5.9M | 1.2M | **31x** |
619
- | large object (100 items) | 13K | 17K | **766K** | 1.3M | 127K | **44x** |
620
- | readonly field (wrapper compiles away) | 3.0M | 6.6M | **16.7M** | — | — | 2.5x |
621
- | readonly root object (rebuild + freeze) | 2.8M | 5.5M | **12.9M** | — | — | 2.3x |
622
- | readonly array (delegates to Zod) | 3.9M | 4.3M | **4.2M** | — | — | 1.0x |
623
- | recursive tree (7 nodes) | 581K | 1.0M | **7.5M** | 11.6M | 4.7M | 7.4x |
624
- | recursive tree (121 nodes) | 31K | 57K | **777K** | 1.9M | 371K | **14x** |
625
- | nested recursion (7 nodes) | 395K | 683K | **7.9M** | 11.2M | 3.0M | **12x** |
626
- | nested recursion (121 nodes) | 24K | 42K | **805K** | 1.6M | 204K | **19x** |
627
- | deeply nested object (243 leaves) | 11K | 27K | **825K** | 1.0M | 125K | **30x** |
628
- | event log (combined) | 363K | 795K | **7.3M** | — | — | 9.2x |
629
- | object with transform (zero-capture) | 1.1M | 1.9M | **6.7M** | — | — | 3.5x |
630
- | array 10 × transform (zero-capture) | 121K | 206K | **4.1M** | — | — | **20x** |
583
+ | simple string | 13.1M | 15.9M | **17.3M** | 18.0M | 17.6M | 1.1x |
584
+ | string (min/max) | 12.3M | 7.3M | **17.3M** | 17.2M | 15.6M | 2.4x |
585
+ | number (int+positive) | 12.4M | 9.7M | **17.5M** | 17.7M | 17.0M | 1.8x |
586
+ | enum | 11.6M | 15.5M | **17.8M** | 18.1M | 17.5M | 1.1x |
587
+ | bigint (min/max) | 11.9M | 8.6M | **17.3M** | — | — | 2.0x |
588
+ | tuple [string, int, bool] | 5.5M | 7.4M | **16.7M** | 16.0M | 15.8M | 2.3x |
589
+ | record\<string, number\> | 3.2M | 2.5M | **12.9M** | 12.0M | 15.2M | 5.1x |
590
+ | set\<string\> (5 items) | 3.6M | 2.3M | **15.3M** | — | — | 6.8x |
591
+ | set\<string\> (20 items) | 1.3M | 679K | **12.2M** | — | — | **18x** |
592
+ | map\<string, number\> (5 entries) | 2.0M | 1.3M | **13.5M** | — | — | **10x** |
593
+ | map\<string, number\> (20 entries) | 628K | 346K | **8.5M** | — | — | **25x** |
594
+ | pipe (non-transform) | 8.8M | 4.7M | **16.9M** | — | — | 3.6x |
595
+ | discriminatedUnion (3 variants) | 3.4M | 5.2M | **17.1M** | 16.5M | 7.9M | 3.3x |
596
+ | discriminatedUnion (8 variants, rotating) | 2.6M | 4.6M | **10.5M** | — | — | 2.3x |
597
+ | plain union of 8 tagged objects (auto-discrim.) | 357K | 1.3M | **10.5M** | — | — | 7.9x |
598
+ | strict object (DB row) | 1.8M | 2.9M | **11.4M** | — | — | 3.8x |
599
+ | medium object (valid) | 1.9M | 2.3M | **9.8M** | 11.1M | 7.6M | 4.2x |
600
+ | medium object (extra keys stripped) | 1.8M | 2.1M | **9.7M** | — | — | 4.6x |
601
+ | medium object (invalid) | 517K | 364K | **15.9M** | 2.8M | 7.5M | **44x** |
602
+ | large object (10 items) | 120K | 175K | **5.3M** | 6.0M | 1.2M | **30x** |
603
+ | large object (100 items) | 13K | 18K | **764K** | 1.3M | 124K | **43x** |
604
+ | readonly field (wrapper compiles away) | 3.1M | 6.6M | **17.8M** | — | — | 2.7x |
605
+ | readonly root object (rebuild + freeze) | 2.9M | 5.6M | **13.5M** | — | — | 2.4x |
606
+ | readonly array (delegates to Zod) | 4.0M | 4.4M | **4.4M** | — | — | 1.0x |
607
+ | recursive tree (7 nodes) | 574K | 997K | **9.0M** | 11.9M | 4.8M | 9.0x |
608
+ | recursive tree (121 nodes) | 32K | 58K | **986K** | 1.9M | 375K | **17x** |
609
+ | nested recursion (7 nodes) | 390K | 696K | **7.8M** | 10.5M | 2.9M | **11x** |
610
+ | nested recursion (121 nodes) | 24K | 42K | **816K** | 1.6M | 205K | **20x** |
611
+ | deeply nested object (243 leaves) | 11K | 28K | **845K** | 1.0M | 125K | **30x** |
612
+ | event log (combined) | 375K | 808K | **7.6M** | — | — | 9.4x |
613
+ | catalog product with image URLs (valid) | 432K | 467K | **2.1M** | — | — | 4.6x |
614
+ | catalog product with image URLs (invalid) | 114K | 106K | **14.8M** | — | — | **140x** |
615
+ | catalog page (20 products) | 21K | 23K | **122K** | — | — | 5.2x |
616
+ | object with transform (zero-capture) | 1.1M | 1.9M | **7.6M** | — | — | 4.0x |
617
+ | array 10 × transform (zero-capture) | 123K | 209K | **4.3M** | — | — | **21x** |
631
618
  | array 50 × transform (zero-capture) | 26K | 41K | **1.0M** | — | — | **25x** |
632
- | object with captured transform | 1.2M | 7.9M | **15.9M** | — | — | 2.0x |
633
- | object with captured refine (cross-field) | 1.4M | 2.2M | **11.5M** | — | — | 5.2x |
634
- | object with superRefine (cross-field) | 1.4M | 2.1M | **9.2M** | — | — | 4.3x |
635
- | coerced query object (valid) | 1.8M | 2.9M | **5.3M** | — | — | 1.9x |
636
- | coerced query object (invalid) | 1.0M | 826K | **10.3M** | — | — | **12x** |
637
- | preprocessed query object (valid) | 433K | 1.7M | **5.3M** | — | — | 3.2x |
638
- | preprocessed query object (invalid) | 392K | 782K | **12.3M** | — | — | **16x** |
619
+ | object with captured transform | 1.2M | 8.3M | **16.2M** | — | — | 2.0x |
620
+ | object with captured refine (cross-field) | 1.5M | 2.2M | **11.7M** | — | — | 5.3x |
621
+ | object with superRefine (cross-field) | 1.5M | 2.2M | **9.6M** | — | — | 4.3x |
622
+ | coerced query object (valid) | 1.8M | 3.0M | **5.4M** | — | — | 1.8x |
623
+ | coerced query object (invalid) | 1.0M | 837K | **10.6M** | — | — | **13x** |
624
+ | preprocessed query object (valid) | 432K | 1.8M | **5.4M** | — | — | 3.0x |
625
+ | preprocessed query object (invalid) | 393K | 796K | **12.7M** | — | — | **16x** |
639
626
  | stringbool config object (valid) | — | 2.8M | **6.3M** | — | — | 2.2x |
640
- | stringbool config object (invalid) | — | 682K | **13.2M** | — | — | **19x** |
641
- | custom/instanceof request (valid) | 965K | 3.1M | **9.9M** | — | — | 3.2x |
642
- | custom/instanceof request (invalid) | 786K | 928K | **13.2M** | — | — | **14x** |
643
- | disjoint object intersection (valid) | 1.4M | 1.6M | **9.5M** | — | — | 5.9x |
644
- | disjoint object intersection (invalid) | 488K | 331K | **15.3M** | — | — | **46x** |
627
+ | stringbool config object (invalid) | — | 698K | **13.5M** | — | — | **19x** |
628
+ | custom/instanceof request (valid) | 979K | 3.2M | **10.4M** | — | — | 3.3x |
629
+ | custom/instanceof request (invalid) | 795K | 955K | **13.6M** | — | — | **14x** |
630
+ | disjoint object intersection (valid) | 1.4M | 1.6M | **9.7M** | — | — | 6.0x |
631
+ | disjoint object intersection (invalid) | 494K | 335K | **15.7M** | — | — | **47x** |
645
632
 
646
633
  _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),
647
634
  best of three runs. The harness costs ~60 ns per iteration, so the fastest rows sit at that floor and gaps
648
635
  between the AOT columns there are noise, not real._
649
636
 
650
637
  Nested objects, arrays and recursive types gain the most. Rejection is fast because a failed
651
- `safeParse` defers building the error until `.error` is read. Zod 4.5 stopped capturing a stack trace on
652
- that path too, so its own rejected-input rows are several times faster than 4.3's and the gap there is
653
- narrower than it was.
638
+ `safeParse` defers building the error until `.error` is read. Zod 4.5 stopped capturing a stack trace
639
+ there too, narrowing the gap on rejected input.
654
640
 
655
641
  ```bash
656
642
  vp run benchmark # run locally
@@ -658,29 +644,22 @@ vp run benchmark # run locally
658
644
 
659
645
  ### Performance Architecture
660
646
 
661
- An eligible schema compiles to a **fast path**, one `&&` chain validating the whole input with zero
662
- allocations and reused by `.is()` and `parse()`, plus a **slow path** that collects errors, runs only on
663
- failure, and is deferred until `.error` is read. A `z.object()` strips, so it instead compiles to a
664
- single pass that validates and rebuilds together and bails on the first failure, covering the reshaping
665
- idioms too (array size checks, `.refine()`, `.default()`, `.trim()`, `.transform()`).
666
-
667
- Regexes are pre-compiled with bounded repeats unrolled, checks run cheapest-first on both passes (so a
668
- payload with a wrong boolean is rejected before its email is scanned, whichever was declared first),
669
- discriminated unions dispatch through a `switch` on the tag (plain tagged unions are auto-discriminated
670
- into it, on the stripping pass as well as the fast check, and the selected option re-checks neither
671
- object-ness nor the tag), and oversized check functions are split to stay within V8's optimizer budget.
672
- `.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
673
- plain-object guard exits on one comparison for an ordinary object, and a case-insensitive `stringbool`
674
- looks its input up verbatim before paying for `toLowerCase()`. Stripping objects, native coercions,
675
- `stringbool`, defaults, string rewrites, context-free preprocessors and synchronous transforms validate
676
- and build their output in one pass. An intersection of two objects with disjoint keys compiles to that
677
- same single pass over the merged shape, and `z.custom()` / `z.instanceof()` compile to a direct predicate
678
- call.
679
-
680
- Where success is cheaper to compile than failure, only the verdict and output are compiled.
681
- Intersections and `custom` keep the original Zod schema to construct issues, so a rejection still
682
- reports exactly what Zod would, an intersection's one-issue-per-side shape included, without slowing the
683
- hot path.
647
+ A schema compiles to a **fast path** — one zero-allocation `&&` chain, shared by `.is()` and
648
+ `parse()` — plus a **slow path** that collects errors only on failure, deferred until `.error` is read.
649
+ Schemas that reshape their input (stripping objects, coercions, `stringbool`, `z.url()`, defaults,
650
+ transforms, context-free preprocessors, disjoint-key intersections) instead validate and rebuild in a
651
+ single pass that bails on the first failure.
652
+
653
+ Other optimizations:
654
+
655
+ - Regexes pre-compiled, bounded repeats unrolled; `z.email()` is a linear scan, not a backtracking regex.
656
+ - Checks run cheapest-first, regardless of declaration order.
657
+ - Discriminated unions dispatch through a `switch`; plain tagged unions are auto-discriminated into one.
658
+ - `z.custom()` / `z.instanceof()` compile to a direct predicate call.
659
+ - Oversized check functions are split to stay within V8's optimizer budget.
660
+
661
+ Intersections and `custom` keep the original Zod schema to construct issues, so rejections match Zod
662
+ exactly without slowing the hot path.
684
663
 
685
664
  ## Development
686
665
 
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"build-path.d.ts","names":[],"sources":["../../../src/core/codegen/build-path.ts"],"mappings":";;;;iBAwMgB,eAAe,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiCnB,kBAAkB,IAAI;;;;;;;;;;;iBA+EtB,sBAAsB,IAAI;;;;;;iBAmD1B,cAAc,IAAI,UAAU,KAAK"}
@@ -1,11 +1,11 @@
1
- import { declareFastTemps, emitEffectCallable, emitEffectFn, emitPooledConstant, emitRuntimeHelper, emitTemp, escapeString, hasMutation, keyMembershipTest, literalToJs, needsProtoScrub, outputAlwaysDefined, rejectsUndefined, tupleRewritesShortInput } from "./context.js";
2
1
  import { ZC_ASYNC_DECL, ZC_HOP_DECL, ZC_PLAIN_DECL, ZC_PROTO_SCRUB_DECL } from "./issue-decls.js";
2
+ import { declareFastTemps, emitEffectCallable, emitEffectFn, emitPooledConstant, emitRuntimeHelper, emitTemp, escapeString, hasMutation, keyMembershipTest, literalToJs, needsProtoScrub, outputAlwaysDefined, rejectsUndefined, tupleRewritesShortInput } from "./context.js";
3
3
  import { defaultValueExpr, needsPostInnerDefault } from "./schemas/default.js";
4
4
  import { detectUnionDiscriminator } from "./schemas/discriminated-union.js";
5
5
  import { estimateFastCost, orderByRuntimeCost, predictedInlineSize } from "./fast-size.js";
6
6
  import { parsedProperties } from "./schemas/object.js";
7
7
  import { innerAppliesDefaultOnUndefined } from "./schemas/optional.js";
8
- import { fastStringCheck } from "./schemas/string.js";
8
+ import { buildUrlCheck, fastStringCheck, isUrlRewrite } from "./schemas/string.js";
9
9
  import { emitStringBoolMap, stringBoolInlineHit, stringBoolUsesInline } from "./schemas/string-bool.js";
10
10
  import { createFastGen, generateFast } from "./fast-path.js";
11
11
  //#region src/core/codegen/build-path.ts
@@ -40,7 +40,7 @@ function rebuildSet(root, includeProtoScrub = true) {
40
40
  for (const node of nodes) {
41
41
  if (rebuilds.has(node)) continue;
42
42
  const target = node.type === "recursiveRef" ? targets.get(node.refId ?? 0) : void 0;
43
- if (node.type === "object" && node.stripUnknownKeys === true || node.type === "readonly" && node.freeze === true || node.type === "default" || node.type === "tuple" && tupleRewritesShortInput(node) || includeProtoScrub && needsProtoScrub(node) || node.type === "stringBool" || node.type === "string" && (node.coerce === true || node.checks.some((c) => c.kind === "overwrite_effect")) || (node.type === "number" || node.type === "boolean" || node.type === "bigint" || node.type === "date") && node.coerce === true || node.type === "effect" || target !== void 0 && rebuilds.has(target) || children(node).some((child) => rebuilds.has(child))) {
43
+ if (node.type === "object" && node.stripUnknownKeys === true || node.type === "readonly" && node.freeze === true || node.type === "default" || node.type === "tuple" && tupleRewritesShortInput(node) || includeProtoScrub && needsProtoScrub(node) || node.type === "stringBool" || node.type === "string" && (node.coerce === true || node.checks.some((c) => c.kind === "overwrite_effect" || isUrlRewrite(c))) || (node.type === "number" || node.type === "boolean" || node.type === "bigint" || node.type === "date") && node.coerce === true || node.type === "effect" || target !== void 0 && rebuilds.has(target) || children(node).some((child) => rebuilds.has(child))) {
44
44
  rebuilds.add(node);
45
45
  changed = true;
46
46
  }
@@ -95,7 +95,7 @@ function fastResultIsInput(ir) {
95
95
  }
96
96
  /**
97
97
  * True when the subtree mutates for any reason the build pass cannot reproduce —
98
- * `.catch()`, `z.url()`, `superRefine`. Those rewrite values in ways this pass
98
+ * `.catch()`, `superRefine`. Those rewrite values in ways this pass
99
99
  * (which validates, coerces, decodes string booleans, substitutes declared
100
100
  * defaults, applies ordered string rewrites and copies) does not model, so the
101
101
  * schema keeps the eager walk.
@@ -110,7 +110,7 @@ function mutatesBeyondStrip(ir) {
110
110
  */
111
111
  function mutatesHere(ir) {
112
112
  switch (ir.type) {
113
- case "string": return superRefines(ir.checks) || ir.checks.some((c) => c.kind === "string_format" && c.format === "url");
113
+ case "string": return superRefines(ir.checks);
114
114
  case "number": return superRefines(ir.checks);
115
115
  case "boolean":
116
116
  case "bigint":
@@ -181,6 +181,9 @@ function generateBuild(ir, ctx) {
181
181
  temps: [],
182
182
  used: 0
183
183
  };
184
+ const name = emitTemp(ctx, "vb");
185
+ const recNames = ctx.buildRecNames ??= /* @__PURE__ */ new Map();
186
+ recNames.set(0, name);
184
187
  const built = build(ir, "input", {
185
188
  ctx,
186
189
  extractable: false,
@@ -188,8 +191,10 @@ function generateBuild(ir, ctx) {
188
191
  rebuilds,
189
192
  scope
190
193
  });
191
- if (built === null) return null;
192
- const name = emitTemp(ctx, "vb");
194
+ if (built === null) {
195
+ recNames.delete(0);
196
+ return null;
197
+ }
193
198
  ctx.preamble.push(`function ${name}(input){${declareFastTemps(scope)}${built.code}return ${built.value};}`);
194
199
  return name;
195
200
  }
@@ -731,6 +736,10 @@ function buildString(ir, input, g) {
731
736
  break;
732
737
  case "super_refine_effect": return null;
733
738
  default: {
739
+ if (check.kind === "string_format" && isUrlRewrite(check)) {
740
+ code += buildUrlCheck(check, value, g.fail, (prefix) => local(g, prefix), g.ctx);
741
+ break;
742
+ }
734
743
  const expr = fastStringCheck(check, value, g.ctx);
735
744
  if (expr === null) return null;
736
745
  code += `if(!(${expr}))return ${g.fail};`;