zod-aot 0.12.0 → 0.14.0

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 (198) hide show
  1. package/README.md +248 -287
  2. package/dist/cli/commands/check.d.ts +4 -2
  3. package/dist/cli/commands/check.d.ts.map +1 -1
  4. package/dist/cli/commands/check.js +102 -113
  5. package/dist/cli/commands/check.js.map +1 -1
  6. package/dist/cli/commands/doctor.d.ts +8 -0
  7. package/dist/cli/commands/doctor.d.ts.map +1 -0
  8. package/dist/cli/commands/doctor.js +136 -0
  9. package/dist/cli/commands/doctor.js.map +1 -0
  10. package/dist/cli/index.js +43 -12
  11. package/dist/cli/index.js.map +1 -1
  12. package/dist/cli/logger.d.ts +10 -0
  13. package/dist/cli/logger.d.ts.map +1 -1
  14. package/dist/cli/logger.js +22 -9
  15. package/dist/cli/logger.js.map +1 -1
  16. package/dist/core/codegen/context.d.ts +3 -10
  17. package/dist/core/codegen/context.d.ts.map +1 -1
  18. package/dist/core/codegen/context.js +10 -31
  19. package/dist/core/codegen/context.js.map +1 -1
  20. package/dist/core/codegen/emit.d.ts +8 -0
  21. package/dist/core/codegen/emit.d.ts.map +1 -0
  22. package/dist/core/codegen/emit.js +16 -0
  23. package/dist/core/codegen/emit.js.map +1 -0
  24. package/dist/core/codegen/fast-check/array.d.ts +4 -0
  25. package/dist/core/codegen/fast-check/array.d.ts.map +1 -0
  26. package/dist/core/codegen/fast-check/array.js +28 -0
  27. package/dist/core/codegen/fast-check/array.js.map +1 -0
  28. package/dist/core/codegen/fast-check/bigint.d.ts +3 -0
  29. package/dist/core/codegen/fast-check/bigint.d.ts.map +1 -0
  30. package/dist/core/codegen/fast-check/bigint.js +19 -0
  31. package/dist/core/codegen/fast-check/bigint.js.map +1 -0
  32. package/dist/core/codegen/fast-check/discriminated-union.d.ts +4 -0
  33. package/dist/core/codegen/fast-check/discriminated-union.d.ts.map +1 -0
  34. package/dist/core/codegen/fast-check/discriminated-union.js +13 -0
  35. package/dist/core/codegen/fast-check/discriminated-union.js.map +1 -0
  36. package/dist/core/codegen/fast-check/enum.d.ts +4 -0
  37. package/dist/core/codegen/fast-check/enum.d.ts.map +1 -0
  38. package/dist/core/codegen/fast-check/enum.js +12 -0
  39. package/dist/core/codegen/fast-check/enum.js.map +1 -0
  40. package/dist/core/codegen/{fast-check.d.ts → fast-check/index.d.ts} +3 -3
  41. package/dist/core/codegen/fast-check/index.d.ts.map +1 -0
  42. package/dist/core/codegen/fast-check/index.js +118 -0
  43. package/dist/core/codegen/fast-check/index.js.map +1 -0
  44. package/dist/core/codegen/fast-check/intersection.d.ts +4 -0
  45. package/dist/core/codegen/fast-check/intersection.d.ts.map +1 -0
  46. package/dist/core/codegen/fast-check/intersection.js +10 -0
  47. package/dist/core/codegen/fast-check/intersection.js.map +1 -0
  48. package/dist/core/codegen/fast-check/literal.d.ts +3 -0
  49. package/dist/core/codegen/fast-check/literal.d.ts.map +1 -0
  50. package/dist/core/codegen/fast-check/literal.js +8 -0
  51. package/dist/core/codegen/fast-check/literal.js.map +1 -0
  52. package/dist/core/codegen/fast-check/nullable.d.ts +4 -0
  53. package/dist/core/codegen/fast-check/nullable.d.ts.map +1 -0
  54. package/dist/core/codegen/fast-check/nullable.js +7 -0
  55. package/dist/core/codegen/fast-check/nullable.js.map +1 -0
  56. package/dist/core/codegen/fast-check/number.d.ts +3 -0
  57. package/dist/core/codegen/fast-check/number.d.ts.map +1 -0
  58. package/dist/core/codegen/fast-check/number.js +51 -0
  59. package/dist/core/codegen/fast-check/number.js.map +1 -0
  60. package/dist/core/codegen/fast-check/object.d.ts +4 -0
  61. package/dist/core/codegen/fast-check/object.d.ts.map +1 -0
  62. package/dist/core/codegen/fast-check/object.js +13 -0
  63. package/dist/core/codegen/fast-check/object.js.map +1 -0
  64. package/dist/core/codegen/fast-check/optional.d.ts +4 -0
  65. package/dist/core/codegen/fast-check/optional.d.ts.map +1 -0
  66. package/dist/core/codegen/fast-check/optional.js +7 -0
  67. package/dist/core/codegen/fast-check/optional.js.map +1 -0
  68. package/dist/core/codegen/fast-check/pipe.d.ts +4 -0
  69. package/dist/core/codegen/fast-check/pipe.d.ts.map +1 -0
  70. package/dist/core/codegen/fast-check/pipe.js +14 -0
  71. package/dist/core/codegen/fast-check/pipe.js.map +1 -0
  72. package/dist/core/codegen/fast-check/readonly.d.ts +4 -0
  73. package/dist/core/codegen/fast-check/readonly.d.ts.map +1 -0
  74. package/dist/core/codegen/fast-check/readonly.js +4 -0
  75. package/dist/core/codegen/fast-check/readonly.js.map +1 -0
  76. package/dist/core/codegen/fast-check/record.d.ts +4 -0
  77. package/dist/core/codegen/fast-check/record.d.ts.map +1 -0
  78. package/dist/core/codegen/fast-check/record.js +18 -0
  79. package/dist/core/codegen/fast-check/record.js.map +1 -0
  80. package/dist/core/codegen/fast-check/string.d.ts +4 -0
  81. package/dist/core/codegen/fast-check/string.d.ts.map +1 -0
  82. package/dist/core/codegen/fast-check/string.js +62 -0
  83. package/dist/core/codegen/fast-check/string.js.map +1 -0
  84. package/dist/core/codegen/fast-check/tuple.d.ts +4 -0
  85. package/dist/core/codegen/fast-check/tuple.d.ts.map +1 -0
  86. package/dist/core/codegen/fast-check/tuple.js +32 -0
  87. package/dist/core/codegen/fast-check/tuple.js.map +1 -0
  88. package/dist/core/codegen/fast-check/union.d.ts +4 -0
  89. package/dist/core/codegen/fast-check/union.d.ts.map +1 -0
  90. package/dist/core/codegen/fast-check/union.js +12 -0
  91. package/dist/core/codegen/fast-check/union.js.map +1 -0
  92. package/dist/core/codegen/generators/array.js +1 -1
  93. package/dist/core/codegen/generators/array.js.map +1 -1
  94. package/dist/core/codegen/generators/bigint.d.ts.map +1 -1
  95. package/dist/core/codegen/generators/bigint.js +2 -1
  96. package/dist/core/codegen/generators/bigint.js.map +1 -1
  97. package/dist/core/codegen/generators/boolean.js +1 -1
  98. package/dist/core/codegen/generators/boolean.js.map +1 -1
  99. package/dist/core/codegen/generators/date.js +1 -1
  100. package/dist/core/codegen/generators/date.js.map +1 -1
  101. package/dist/core/codegen/generators/default.js +1 -1
  102. package/dist/core/codegen/generators/default.js.map +1 -1
  103. package/dist/core/codegen/generators/discriminated-union.d.ts.map +1 -1
  104. package/dist/core/codegen/generators/discriminated-union.js +2 -1
  105. package/dist/core/codegen/generators/discriminated-union.js.map +1 -1
  106. package/dist/core/codegen/generators/enum.d.ts.map +1 -1
  107. package/dist/core/codegen/generators/enum.js +3 -2
  108. package/dist/core/codegen/generators/enum.js.map +1 -1
  109. package/dist/core/codegen/generators/fallback.js +1 -1
  110. package/dist/core/codegen/generators/fallback.js.map +1 -1
  111. package/dist/core/codegen/generators/index.d.ts +3 -33
  112. package/dist/core/codegen/generators/index.d.ts.map +1 -1
  113. package/dist/core/codegen/generators/index.js +103 -33
  114. package/dist/core/codegen/generators/index.js.map +1 -1
  115. package/dist/core/codegen/generators/literal.d.ts.map +1 -1
  116. package/dist/core/codegen/generators/literal.js +2 -1
  117. package/dist/core/codegen/generators/literal.js.map +1 -1
  118. package/dist/core/codegen/generators/map.js +1 -1
  119. package/dist/core/codegen/generators/map.js.map +1 -1
  120. package/dist/core/codegen/generators/nan.js +1 -1
  121. package/dist/core/codegen/generators/nan.js.map +1 -1
  122. package/dist/core/codegen/generators/never.js +1 -1
  123. package/dist/core/codegen/generators/never.js.map +1 -1
  124. package/dist/core/codegen/generators/null.js +1 -1
  125. package/dist/core/codegen/generators/null.js.map +1 -1
  126. package/dist/core/codegen/generators/nullable.js +1 -1
  127. package/dist/core/codegen/generators/nullable.js.map +1 -1
  128. package/dist/core/codegen/generators/number.d.ts.map +1 -1
  129. package/dist/core/codegen/generators/number.js +2 -1
  130. package/dist/core/codegen/generators/number.js.map +1 -1
  131. package/dist/core/codegen/generators/object.d.ts.map +1 -1
  132. package/dist/core/codegen/generators/object.js +2 -1
  133. package/dist/core/codegen/generators/object.js.map +1 -1
  134. package/dist/core/codegen/generators/optional.js +1 -1
  135. package/dist/core/codegen/generators/optional.js.map +1 -1
  136. package/dist/core/codegen/generators/record.js +1 -1
  137. package/dist/core/codegen/generators/record.js.map +1 -1
  138. package/dist/core/codegen/generators/recursive-ref.js +1 -1
  139. package/dist/core/codegen/generators/recursive-ref.js.map +1 -1
  140. package/dist/core/codegen/generators/set.js +1 -1
  141. package/dist/core/codegen/generators/set.js.map +1 -1
  142. package/dist/core/codegen/generators/string.d.ts.map +1 -1
  143. package/dist/core/codegen/generators/string.js +2 -1
  144. package/dist/core/codegen/generators/string.js.map +1 -1
  145. package/dist/core/codegen/generators/symbol.js +1 -1
  146. package/dist/core/codegen/generators/symbol.js.map +1 -1
  147. package/dist/core/codegen/generators/template-literal.d.ts.map +1 -1
  148. package/dist/core/codegen/generators/template-literal.js +2 -1
  149. package/dist/core/codegen/generators/template-literal.js.map +1 -1
  150. package/dist/core/codegen/generators/tuple.js +1 -1
  151. package/dist/core/codegen/generators/tuple.js.map +1 -1
  152. package/dist/core/codegen/generators/undefined.js +1 -1
  153. package/dist/core/codegen/generators/undefined.js.map +1 -1
  154. package/dist/core/codegen/generators/union.js +1 -1
  155. package/dist/core/codegen/generators/union.js.map +1 -1
  156. package/dist/core/codegen/generators/void.js +1 -1
  157. package/dist/core/codegen/generators/void.js.map +1 -1
  158. package/dist/core/codegen/index.d.ts +1 -1
  159. package/dist/core/codegen/index.d.ts.map +1 -1
  160. package/dist/core/codegen/index.js +3 -73
  161. package/dist/core/codegen/index.js.map +1 -1
  162. package/dist/core/compile.d.ts.map +1 -1
  163. package/dist/core/compile.js +12 -1
  164. package/dist/core/compile.js.map +1 -1
  165. package/dist/core/diagnostic.d.ts +46 -0
  166. package/dist/core/diagnostic.d.ts.map +1 -0
  167. package/dist/core/diagnostic.js +168 -0
  168. package/dist/core/diagnostic.js.map +1 -0
  169. package/dist/core/iife.d.ts.map +1 -1
  170. package/dist/core/iife.js +7 -1
  171. package/dist/core/iife.js.map +1 -1
  172. package/dist/core/pipeline.d.ts +10 -4
  173. package/dist/core/pipeline.d.ts.map +1 -1
  174. package/dist/core/pipeline.js +21 -9
  175. package/dist/core/pipeline.js.map +1 -1
  176. package/dist/core/types.d.ts +4 -0
  177. package/dist/core/types.d.ts.map +1 -1
  178. package/dist/discovery.d.ts +10 -6
  179. package/dist/discovery.d.ts.map +1 -1
  180. package/dist/discovery.js +18 -2
  181. package/dist/discovery.js.map +1 -1
  182. package/dist/unplugin/index.d.ts.map +1 -1
  183. package/dist/unplugin/index.js +41 -17
  184. package/dist/unplugin/index.js.map +1 -1
  185. package/dist/unplugin/transform.d.ts +28 -4
  186. package/dist/unplugin/transform.d.ts.map +1 -1
  187. package/dist/unplugin/transform.js +134 -12
  188. package/dist/unplugin/transform.js.map +1 -1
  189. package/dist/unplugin/types.d.ts +16 -0
  190. package/dist/unplugin/types.d.ts.map +1 -1
  191. package/package.json +2 -1
  192. package/dist/core/codegen/fast-check.d.ts.map +0 -1
  193. package/dist/core/codegen/fast-check.js +0 -406
  194. package/dist/core/codegen/fast-check.js.map +0 -1
  195. package/dist/core/runtime.d.ts +0 -10
  196. package/dist/core/runtime.d.ts.map +0 -1
  197. package/dist/core/runtime.js +0 -14
  198. package/dist/core/runtime.js.map +0 -1
package/README.md CHANGED
@@ -7,408 +7,369 @@
7
7
  [![npm](https://img.shields.io/npm/v/zod-aot)](https://www.npmjs.com/package/zod-aot)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
9
9
 
10
- No code changes required — keep your existing Zod schemas and get **2-64x faster** validation.
10
+ Keep your existing Zod schemas. Get **2-64x faster** validation. No code changes required.
11
11
 
12
- ## Why
13
-
14
- Zod v4 is already fast, but runtime schema traversal still costs ~10x compared to ahead-of-time (AOT) compiled approaches. Existing AOT solutions like [Typia](https://typia.io/) require rewriting your schemas as TypeScript types. **zod-aot** bridges this gap — it takes your existing Zod schemas and generates optimized, plain JavaScript validation functions at build time.
12
+ ```typescript
13
+ // vite.config.ts — add one line
14
+ import zodAot from "zod-aot/vite";
15
+ export default defineConfig({
16
+ plugins: [zodAot({ autoDiscover: true })],
17
+ });
18
+ ```
15
19
 
16
- | | Zod AOT | Typia | AJV standalone | Zod v3 | Zod v4 |
17
- |---|---------------------|---|---|---|---|
18
- | **Input** | Zod schemas | TS types | JSON Schema | Zod schemas | Zod schemas |
19
- | **Existing code changes** | None | Full rewrite | Full rewrite | N/A | N/A |
20
- | **Type inference** | Inherited from Zod | Native | External | Native | Native |
21
- | **Runtime dependency** | None (generated code) | Typia runtime | AJV runtime | Zod | Zod |
20
+ ```typescript
21
+ // src/schemas.ts — write plain Zod, nothing else
22
+ import { z } from "zod";
22
23
 
23
- ## Benchmarks
24
+ export const UserSchema = z.object({
25
+ name: z.string().min(3),
26
+ email: z.email(),
27
+ age: z.number().int().positive(),
28
+ });
24
29
 
25
- Measured with `vitest bench` on Node.js (Apple M-series). The benchmark suite compares **Zod v3**, **Zod v4**, **zod-aot**, **[ajv](https://ajv.js.org/)**, and **[typia](https://typia.io/)** across primitives, objects, collections, unions, recursive schemas, and real-world scenarios.
30
+ // Use it anywhere tRPC, Hono, React Hook Form, etc.
31
+ // At build time, zod-aot compiles it to a 3-64x faster validator.
32
+ ```
26
33
 
27
- Run benchmarks locally:
34
+ ## Install
28
35
 
29
36
  ```bash
30
- pnpm bench
37
+ npm install zod-aot zod@^4
31
38
  ```
32
39
 
33
- ### safeParse
40
+ | Runtime | Version |
41
+ |---------|---------|
42
+ | Node.js | 20+ |
43
+ | Bun | 1.3+ |
44
+ | Deno | 2.0+ |
34
45
 
35
- | Scenario | Zod v3 | Zod v4 | **Zod AOT** | Typia | AJV | vs Zod v4 |
36
- |---|---|---|---|---|---|---|
37
- | simple string | 8.7M | 10.0M | **11.0M** | 11.0M | 11.1M | 1.1x |
38
- | string (min/max) | 8.3M | 6.0M | **11.1M** | 11.3M | 9.5M | 1.8x |
39
- | number (int+positive) | 8.2M | 5.8M | **10.9M** | 10.7M | 10.9M | 1.9x |
40
- | enum | 8.0M | 9.5M | **10.6M** | 10.6M | 10.5M | 1.1x |
41
- | bigint (min/max) | 8.0M | 6.0M | **10.9M** | — | — | 1.8x |
42
- | tuple [string, int, bool] | 4.0M | 4.4M | **10.5M** | 10.8M | 10.5M | 2.4x |
43
- | record\<string, number\> | 2.3M | 1.9M | **5.4M** | 7.5M | 9.4M | 2.8x |
44
- | set\<string\> (5 items) | 2.7M | 1.6M | **9.8M** | — | — | 6.3x |
45
- | set\<string\> (20 items) | 1.0M | 475K | **7.7M** | — | — | **16x** |
46
- | map\<string, number\> (5 entries) | 1.5M | 946K | **8.5M** | — | — | 9.0x |
47
- | map\<string, number\> (20 entries) | 490K | 238K | **5.2M** | — | — | **22x** |
48
- | pipe (non-transform) | 6.3M | 3.8M | **10.8M** | — | — | 2.8x |
49
- | discriminatedUnion (3 variants) | 2.3M | 2.9M | **9.8M** | 10.4M | 5.6M | 3.4x |
50
- | medium object (valid) | 1.3M | 1.7M | **5.4M** | 7.3M | 5.0M | 3.1x |
51
- | medium object (invalid) | 351K | 65K | **471K** | 2.1M | 5.6M | 7.3x |
52
- | large object (10 items) | 82K | 111K | **4.0M** | 4.1M | 834K | **36x** |
53
- | large object (100 items) | 9.0K | 11.6K | **676K** | 808K | 89K | **58x** |
54
- | recursive tree (7 nodes) | 424K | 1.5M | **6.4M** | 8.1M | 3.1M | 4.1x |
55
- | recursive tree (121 nodes) | 24K | 101K | **741K** | 1.4M | 250K | 7.4x |
56
- | event log (combined) | 260K | 479K | **4.5M** | — | — | 9.4x |
57
- | partial fallback obj (transform) | 817K | 1.4M | **3.3M** | — | — | 2.3x |
58
- | partial fallback arr 10 (transform) | 88K | 139K | **841K** | — | — | 6.1x |
59
- | partial fallback arr 50 (transform) | 18K | 28K | **175K** | — | — | 6.3x |
46
+ ## Usage
60
47
 
61
- *ops/s, higher is better. "—" = not supported by the library. Measured with `vitest bench` on Apple M-series.*
48
+ There are three ways to use zod-aot. Choose the one that fits your project.
62
49
 
63
- Performance gains scale with schema complexity. The `discriminatedUnion` optimization uses an O(1) `switch` dispatch instead of Zod's sequential trial approach. Partial fallback schemas (containing `transform`/`refine`) still show 2-6x speedups by compiling the optimizable portions. Set/Map/BigInt are only benchmarked among Zod variants as AJV and Typia lack native support for these types.
50
+ ### 1. autoDiscover (Recommended)
64
51
 
65
- ## Runtime Support
52
+ The plugin automatically detects and compiles all exported Zod schemas at build time. No wrappers, no imports from `zod-aot` in your source code.
66
53
 
67
- | Runtime | Version | Status |
68
- |---------|---------|--------|
69
- | Node.js | 20+ | Fully supported |
70
- | Bun | 1.3+ | Fully supported |
71
- | Deno | 2.0+ | Fully supported |
54
+ **vite.config.ts:**
72
55
 
73
- ## Install
56
+ ```typescript
57
+ import zodAot from "zod-aot/vite";
74
58
 
75
- ```bash
76
- npm install zod-aot
77
- # zod v4 is a peer dependency
78
- npm install zod@^4
59
+ export default defineConfig({
60
+ plugins: [zodAot({ autoDiscover: true })],
61
+ });
79
62
  ```
80
63
 
81
- ## Quick Start
82
-
83
- ### 1. Define schemas with `compile()`
64
+ **Your schema file stays pure Zod:**
84
65
 
85
66
  ```typescript
86
67
  // src/schemas.ts
87
68
  import { z } from "zod";
88
- import { compile } from "zod-aot";
89
69
 
90
- const UserSchema = z.object({
91
- name: z.string().min(3),
92
- age: z.number().int().positive(),
70
+ export const CreateUserSchema = z.object({
71
+ name: z.string().min(1).max(100),
93
72
  email: z.email(),
94
- role: z.enum(["admin", "user"]),
73
+ age: z.number().int().min(0).max(150),
74
+ role: z.enum(["admin", "editor", "viewer"]),
95
75
  });
96
76
 
97
- // compile() falls back to Zod in dev, uses generated functions after build
98
- export const validateUser = compile(UserSchema);
77
+ export const UpdateUserSchema = z.object({
78
+ name: z.string().min(1).max(100).optional(),
79
+ email: z.email().optional(),
80
+ });
81
+
82
+ export const ListUsersSchema = z.object({
83
+ page: z.number().int().min(1).optional().default(1),
84
+ limit: z.number().int().min(1).max(100).optional().default(20),
85
+ });
99
86
  ```
100
87
 
101
- ### 2. Use the compiled validator
88
+ **Use them as usual:**
102
89
 
103
90
  ```typescript
104
- // Same interface as Zod works in both dev and production
105
- const user = validateUser.parse(data); // throws on failure
106
- const result = validateUser.safeParse(data); // { success, data/error }
91
+ const user = CreateUserSchema.parse(data); // throws on failure
92
+ const result = CreateUserSchema.safeParse(data); // { success, data/error }
107
93
  ```
108
94
 
109
- ### 3. Generate optimized code
95
+ At build time, the plugin:
96
+ 1. Finds every file with `import ... from "zod"` (skips type-only imports)
97
+ 2. Executes the file and detects exported Zod schemas
98
+ 3. Compiles each schema into an optimized validator
99
+ 4. Replaces the export with a tree-shakeable IIFE that preserves the full Zod API
100
+
101
+ **What "preserves the full Zod API" means:** The compiled schema inherits from the original Zod schema via `Object.create()`. So `._zod`, `.shape`, Standard Schema (`~standard`), `instanceof` checks — all still work. Libraries that accept Zod schemas (tRPC, Hono, React Hook Form) work without changes.
110
102
 
111
- Choose one of these approaches:
103
+ ### 2. compile() (Explicit)
112
104
 
113
- **Option A: Build plugin (Vite / webpack / esbuild / Rollup / Rolldown / rspack)**
105
+ If you prefer explicit opt-in, wrap specific schemas with `compile()`:
114
106
 
115
107
  ```typescript
116
- // vite.config.ts
117
- import zodAot from "zod-aot/vite";
108
+ import { z } from "zod";
109
+ import { compile } from "zod-aot";
118
110
 
119
- export default defineConfig({
120
- plugins: [zodAot()],
111
+ const UserSchema = z.object({
112
+ name: z.string().min(3),
113
+ email: z.email(),
121
114
  });
115
+
116
+ export const validateUser = compile(UserSchema);
117
+
118
+ // In dev: falls back to Zod's runtime validation
119
+ // After build: uses AOT-compiled optimized code
120
+ validateUser.parse(data);
121
+ validateUser.safeParse(data);
122
122
  ```
123
123
 
124
- Also available: `zod-aot/webpack`, `zod-aot/esbuild`, `zod-aot/rollup`, `zod-aot/rolldown`, `zod-aot/rspack`, `zod-aot/bun`
124
+ `compile()` and `autoDiscover` coexist `compile()` schemas are detected first, then `autoDiscover` picks up remaining plain Zod exports.
125
+
126
+ ### 3. CLI (No Bundler)
125
127
 
126
- **Option B: CLI**
128
+ Generate optimized validation files from the command line:
127
129
 
128
130
  ```bash
129
- # Generate optimized validators
131
+ # Single file
130
132
  npx zod-aot generate src/schemas.ts -o src/schemas.compiled.ts
131
133
 
132
- # Generate from a directory
134
+ # Directory
133
135
  npx zod-aot generate src/ -o src/compiled/
134
136
 
135
- # Watch mode — regenerate on file changes
137
+ # Watch mode
136
138
  npx zod-aot generate src/ --watch
137
-
138
- # Check if schemas are compilable (without generating)
139
- npx zod-aot check src/schemas.ts
140
139
  ```
141
140
 
142
- ## Build Plugin (unplugin)
141
+ ## Build Plugin
143
142
 
144
- The build plugin automatically replaces `compile()` calls with optimized inline validators during the build step. No code changes needed — your source files stay the same.
143
+ ### Supported Build Tools
145
144
 
146
- ```typescript
147
- // vite.config.ts
148
- import zodAot from "zod-aot/vite";
149
- export default defineConfig({ plugins: [zodAot()] });
145
+ | Build Tool | Import |
146
+ |---|---|
147
+ | Vite | `import zodAot from "zod-aot/vite"` |
148
+ | webpack | `import zodAot from "zod-aot/webpack"` |
149
+ | esbuild | `import zodAot from "zod-aot/esbuild"` |
150
+ | Rollup | `import zodAot from "zod-aot/rollup"` |
151
+ | Rolldown | `import zodAot from "zod-aot/rolldown"` |
152
+ | rspack | `import zodAot from "zod-aot/rspack"` |
153
+ | Bun | `import zodAot from "zod-aot/bun"` |
154
+
155
+ ### Options
150
156
 
151
- // webpack.config.js
152
- const zodAot = require("zod-aot/webpack");
153
- module.exports = { plugins: [zodAot()] };
157
+ | Option | Type | Default | Description |
158
+ |---|---|---|---|
159
+ | `autoDiscover` | `boolean` | `false` | Auto-detect all exported Zod schemas without `compile()` |
160
+ | `include` | `string[]` | — | Only process files matching these substrings |
161
+ | `exclude` | `string[]` | — | Skip files matching these substrings |
162
+ | `zodCompat` | `boolean` | `true` | Preserve Zod API via `Object.create()`. Set `false` for smaller output |
163
+ | `verbose` | `boolean` | `false` | Log per-schema compilation status during build |
164
+
165
+ ```typescript
166
+ zodAot({
167
+ autoDiscover: true,
168
+ include: ["src/schemas"],
169
+ verbose: true,
170
+ })
154
171
  ```
155
172
 
156
- ### Options
173
+ ### autoDiscover: Side Effects Warning
174
+
175
+ With `autoDiscover`, the plugin executes files to inspect their exports. If a file imports Zod AND has side effects (starts a server, connects to a database), those side effects run at build time.
176
+
177
+ **Fix:** Use `include` to limit which files are scanned:
157
178
 
158
179
  ```typescript
159
180
  zodAot({
160
- include: ["src/schemas"], // only process files matching these substrings
161
- exclude: ["test", "mock"], // skip files matching these substrings
181
+ autoDiscover: true,
182
+ include: ["src/schemas", "src/validators"],
162
183
  })
163
184
  ```
164
185
 
165
- The plugin:
166
- - Runs at build time (`enforce: "pre"`)
167
- - Replaces `compile(Schema)` with optimized IIFE inline validators
168
- - Adds `/* @__PURE__ */` annotations for tree-shaking
169
- - Supports HMR in development
186
+ ### autoDiscover vs compile()
170
187
 
171
- ## Zod Ecosystem Compatibility
188
+ | | autoDiscover | compile() |
189
+ |---|---|---|
190
+ | Source code changes | None | Wrap each schema |
191
+ | `zod-aot` import needed | No | Yes |
192
+ | What gets compiled | All exported Zod schemas | Only wrapped schemas |
193
+ | Build-time file execution | Files with `import ... from "zod"` | Files with `import ... from "zod-aot"` |
194
+ | Best for | New projects, framework integration | Gradual adoption, selective optimization |
172
195
 
173
- `compile()` returns a full Zod schema (`T & CompiledSchema<T>`) — it preserves all original Zod methods and only replaces validation methods (`safeParse`, `parse`, etc.) with AOT-optimized versions. This means compiled schemas work with any library that accepts Zod schemas.
196
+ ## Framework Examples
174
197
 
175
- ### Hono
198
+ ### tRPC
176
199
 
177
200
  ```typescript
178
- import { Hono } from "hono";
179
- import { zValidator } from "@hono/zod-validator";
201
+ // src/schemas.ts
180
202
  import { z } from "zod";
181
- import { compile } from "zod-aot";
182
203
 
183
- const UserSchema = z.object({
184
- name: z.string().min(3),
185
- age: z.number().int().positive(),
204
+ export const CreateUserSchema = z.object({
205
+ name: z.string().min(1).max(100),
186
206
  email: z.email(),
207
+ age: z.number().int().min(0).max(150),
187
208
  });
188
209
 
189
- // Still a Zod schema — but safeParse is AOT-optimized
190
- const validateUser = compile(UserSchema);
210
+ // src/router.ts
211
+ import { CreateUserSchema } from "./schemas";
212
+
213
+ export const appRouter = t.router({
214
+ createUser: t.procedure
215
+ .input(CreateUserSchema)
216
+ .mutation(({ input }) => createUser(input)),
217
+ });
218
+ ```
219
+
220
+ With `autoDiscover: true`, `CreateUserSchema` is compiled at build time. The tRPC router uses the optimized version automatically. No `.input(compile(CreateUserSchema))` needed.
221
+
222
+ ### Hono
223
+
224
+ ```typescript
225
+ import { Hono } from "hono";
226
+ import { zValidator } from "@hono/zod-validator";
227
+ import { UserSchema } from "./schemas";
191
228
 
192
229
  const app = new Hono();
193
230
 
194
- app.post("/users", zValidator("json", validateUser), (c) => {
231
+ app.post("/users", zValidator("json", UserSchema), (c) => {
195
232
  const user = c.req.valid("json");
196
233
  return c.json(user);
197
234
  });
198
235
  ```
199
236
 
200
- `@hono/zod-validator` internally calls `schema.safeParse()` — with zod-aot, this call is replaced by the generated optimized validator at build time, giving you faster request validation with zero code changes.
237
+ ### React Hook Form
201
238
 
202
- ## How It Works
203
-
204
- ```
205
- Zod Schema
206
-
207
-
208
- ┌─────────────────────┐
209
- │ Extractor │ Traverse _zod.def recursively
210
- extractSchema() │ → produce JSON-serializable IR
211
- └─────────┬───────────┘
212
- │ SchemaIR
213
-
214
- ┌─────────────────────┐
215
- │ CodeGen │ IR → optimized JS with:
216
- │ generateValidator()│ • inline type checks
217
- └─────────┬───────────┘ • pre-compiled RegExps
218
- │ • Set-based enum lookups
219
- ▼ • early returns
220
- Optimized JS function
221
- (no runtime dependencies)
239
+ ```typescript
240
+ import { useForm } from "react-hook-form";
241
+ import { zodResolver } from "@hookform/resolvers/zod";
242
+ import { UserSchema } from "./schemas";
243
+
244
+ function UserForm() {
245
+ const form = useForm({
246
+ resolver: zodResolver(UserSchema),
247
+ });
248
+ // ...
249
+ }
222
250
  ```
223
251
 
224
- ### Why Runtime Extraction (not static analysis)
252
+ ### Any Standard Schema Consumer
225
253
 
226
- - Zod v4's `_zod.def` is JSON-serializable perfect for IR extraction
227
- - Static AST analysis cannot handle dynamic schemas (variable references, function calls, spread operators)
228
- - `_zod.bag` contains aggregated metadata from checks (minimum, maximum, regex patterns, etc.)
229
- - Reliable detection of `transform`/`refine` for automatic fallback
254
+ zod-aot compiled schemas implement [Standard Schema](https://standardschema.dev) via prototype chain. Any library that accepts Standard Schema validators works automatically.
230
255
 
231
- ### Generated Code Example
256
+ ## Schema Diagnostics
232
257
 
233
- **Input:**
258
+ Analyze your schemas before compiling — check coverage, Fast Path eligibility, and get actionable hints:
234
259
 
235
- ```typescript
236
- z.object({
237
- name: z.string().min(3).max(50),
238
- role: z.enum(["admin", "user"]),
239
- })
260
+ ```bash
261
+ npx zod-aot check src/schemas.ts
240
262
  ```
241
263
 
242
- **Output:**
243
-
244
- ```javascript
245
- /* zod-aot */
246
- var __set_0 = new Set(["admin", "user"]);
247
-
248
- function safeParse_validate(input) {
249
- var __issues = [];
250
- if (typeof input !== "object" || input === null || Array.isArray(input)) {
251
- __issues.push({ code: "invalid_type", expected: "object", path: [] });
252
- } else {
253
- // name: string().min(3).max(50)
254
- if (typeof input["name"] !== "string") {
255
- __issues.push({ code: "invalid_type", expected: "string", path: ["name"] });
256
- } else {
257
- if (input["name"].length < 3)
258
- __issues.push({ code: "too_small", minimum: 3, path: ["name"] });
259
- if (input["name"].length > 50)
260
- __issues.push({ code: "too_big", maximum: 50, path: ["name"] });
261
- }
262
-
263
- // role: enum(["admin", "user"])
264
- if (!__set_0.has(input["role"])) {
265
- __issues.push({ code: "invalid_enum_value", path: ["role"] });
266
- }
267
- }
268
- if (__issues.length > 0) return { success: false, error: { issues: __issues } };
269
- return { success: true, data: input };
270
- }
271
- ```
264
+ Output:
272
265
 
273
- Key optimizations in the generated code:
266
+ ```
267
+ src/schemas.ts
274
268
 
275
- - **No schema traversal** — all validation logic is inlined
276
- - **Pre-compiled RegExps** — regex patterns are compiled once, reused across calls
277
- - **Set-based enum lookups** — O(1) membership tests instead of array iteration
278
- - **Early type checks** — nested checks only run if the type is correct
279
- - **Minimal allocations** — issues array is only created once per call
269
+ CreateUserSchema [Fast Path] 100% compiled (5 checks)
270
+ ├─ name: string (min_length, max_length)
271
+ ├─ email: string (string_format[email])
272
+ ├─ age: number (number_format[safeint], greater_than)
273
+ └─ role: enum
280
274
 
281
- ## API Reference
275
+ OrderSchema [Slow Path] 85% compiled (3 checks)
276
+ ├─ id: string (string_format[uuid])
277
+ └─ metadata: object
278
+ └─ audit: fallback(transform)
279
+ Hint: Consider z.pipe() to keep inner schema compilable
282
280
 
283
- ### `compile<T>(zodSchema): CompiledSchema<T>`
281
+ Summary: 2 schemas | 1 Fast Path, 1 Slow Path | 8/9 nodes (88.9%)
282
+ ```
284
283
 
285
- Wraps a Zod schema for AOT compilation. In development, it falls back to Zod's built-in validation. After build (via CLI or unplugin), it uses the generated optimized validator.
284
+ ### CI Integration
286
285
 
287
- ```typescript
288
- import { z } from "zod";
289
- import { compile } from "zod-aot";
286
+ ```bash
287
+ # JSON output
288
+ npx zod-aot check src/schemas.ts --json
290
289
 
291
- const validateUser = compile(z.object({
292
- name: z.string().min(3),
293
- age: z.number().int().positive(),
294
- }));
290
+ # Fail if any schema below 80% coverage
291
+ npx zod-aot check src/schemas.ts --json --fail-under 80
295
292
  ```
296
293
 
297
- ### `CompiledSchema<T>`
294
+ | Flag | Description |
295
+ |---|---|
296
+ | `--json` | Structured JSON output |
297
+ | `--fail-under <pct>` | Exit code 1 if coverage below threshold |
298
+ | `--no-color` | Disable colored output |
298
299
 
299
- The interface returned by `compile()`:
300
+ ## What Gets Compiled
300
301
 
301
- ```typescript
302
- interface CompiledSchema<T> {
303
- parse(input: unknown): T; // throws on failure
304
- safeParse(input: unknown): SafeParseResult<T>; // { success, data/error }
305
- schema: unknown; // reference to original Zod schema
306
- }
307
- ```
302
+ ### Fully Compiled (3-64x faster)
308
303
 
309
- ## Supported Types
304
+ `string`, `number`, `bigint`, `boolean`, `null`, `undefined`, `any`, `unknown`, `literal`, `enum`, `date`, `object`, `array`, `tuple`, `record`, `set`, `map`, `union`, `discriminatedUnion`, `intersection`, `pipe` (non-transform), `optional`, `nullable`, `readonly`, `default`, `catch`, `coerce`, `templateLiteral`, `symbol`, `void`, `nan`, `never`, `lazy` (self-recursive)
310
305
 
311
- | Type | Supported Checks |
312
- |---|---|
313
- | `string` | `min`, `max`, `length`, `email`, `url`, `uuid`, `regex`, `includes`, `startsWith`, `endsWith` |
314
- | `number` | `int`, `positive`, `negative`, `nonnegative`, `nonpositive`, `min`, `max`, `multipleOf`, `int32`, `uint32`, `float32`, `float64` |
315
- | `bigint` | `min`, `max`, `positive`, `negative`, `nonnegative`, `nonpositive`, `multipleOf` |
316
- | `boolean` | — |
317
- | `null` | — |
318
- | `undefined` | — |
319
- | `any` | — (always passes) |
320
- | `unknown` | — (always passes) |
321
- | `literal` | single value, multi-value |
322
- | `enum` | string enum values |
323
- | `date` | `min`, `max` (timestamp comparison) |
324
- | `object` | nested objects, mixed property types |
325
- | `array` | `min`, `max`, `length`, element validation |
326
- | `tuple` | per-element types, optional rest element |
327
- | `record` | key and value type validation |
328
- | `set` | `min`, `max` (size), element validation |
329
- | `map` | key and value type validation |
330
- | `union` | sequential trial of all options |
331
- | `discriminatedUnion` | O(1) `switch` dispatch on discriminator field |
332
- | `intersection` | validates both left and right schemas |
333
- | `pipe` (non-transform) | sequential in→out validation |
334
- | `optional` | wraps any supported type |
335
- | `nullable` | wraps any supported type |
336
- | `readonly` | validates inner type (TS-only concept) |
337
- | `default` | replaces `undefined` with default value, then validates inner |
338
- | `symbol` | — (typeof check) |
339
- | `void` | — (accepts `undefined`) |
340
- | `nan` | — (Number.isNaN check) |
341
- | `never` | — (always fails) |
342
- | `lazy` (self-recursive) | cycle detection → self-recursive codegen |
343
-
344
- ### Automatic Fallback to Zod
345
-
346
- These schema types contain JavaScript closures or runtime-dependent logic that cannot be compiled to static code. They are detected during extraction and produce a `FallbackIR`:
347
-
348
- - `transform` — runtime data transformation
349
- - `refine` / `superRefine` — custom validation with closures
350
- - `custom` — arbitrary validation logic
351
- - `preprocess` — input preprocessing
352
- - `lazy` (non-recursive) — deferred schema resolution where inner type cannot be resolved
353
-
354
- #### Partial Fallback
355
-
356
- When a schema contains a mix of compilable and non-compilable parts (e.g., an object where some properties use `transform`/`refine`), zod-aot compiles the optimizable parts and delegates only the non-compilable properties to Zod at runtime.
357
-
358
- > **Note:** If a schema heavily relies on `transform`, `refine`, or other non-compilable features, the performance benefit from partial fallback will be minimal — most of the validation work is still delegated to Zod. Partial fallback is most effective when only a small portion of the schema uses these features.
359
-
360
- ## Development
306
+ All standard Zod checks are supported: `min`, `max`, `length`, `email`, `url`, `uuid`, `regex`, `int`, `positive`, `negative`, `multipleOf`, `int32`, `uint32`, `float32`, `float64`, `includes`, `startsWith`, `endsWith`, and more.
361
307
 
362
- ```bash
363
- # Install dependencies
364
- pnpm install
308
+ ### Falls Back to Zod (Still Works, Not Faster)
365
309
 
366
- # Run tests
367
- pnpm test
310
+ These types contain JavaScript closures that cannot be compiled to static code:
368
311
 
369
- # Run benchmarks (zod v3 vs zod v4 vs zod-aot vs ajv vs typia)
370
- pnpm bench
312
+ | Type | Why | Alternative |
313
+ |---|---|---|
314
+ | `transform` | Runtime data transformation function | Use `z.pipe()` when possible |
315
+ | `refine` / `superRefine` | Custom validation closures | Use built-in checks when possible |
316
+ | `custom` | Arbitrary validation logic | — |
317
+ | `preprocess` | Input preprocessing function | Use `z.coerce` when possible |
318
+ | `lazy` (non-recursive) | Cannot resolve inner type | Use self-referencing lazy for recursion |
371
319
 
372
- # Lint (Biome)
373
- pnpm lint
320
+ **Partial fallback:** If an object has 10 properties and 1 uses `transform`, the other 9 are still compiled. Only the `transform` property falls back to Zod.
374
321
 
375
- # Type check
376
- pnpm -r typecheck
322
+ **Tip:** Run `npx zod-aot check` to see exactly which parts of your schemas are compiled and which fall back.
377
323
 
378
- # Build
379
- pnpm -r build
380
- ```
324
+ ## Benchmarks
381
325
 
382
- ### Project Structure
326
+ 5-way comparison: **Zod v3** vs **Zod v4** vs **Zod AOT** vs **[Typia](https://typia.io/)** vs **[AJV](https://ajv.js.org/)**
383
327
 
384
- ```
385
- zod-aot/
386
- ├── packages/zod-aot/ # Main npm package
387
- │ ├── src/
388
- │ │ ├── index.ts # Public API exports (zod-aot)
389
- │ │ ├── discovery.ts # Schema discovery (shared by CLI & unplugin)
390
- │ │ ├── loader.ts # Runtime-aware file loader
391
- │ │ ├── core/ # Pure logic (no CLI/unplugin deps)
392
- │ │ │ ├── types.ts # SchemaIR, CompiledSchema, CheckIR
393
- │ │ │ ├── compile.ts # compile() marker + isCompiledSchema()
394
- │ │ │ ├── runtime.ts # createFallback (dev-time)
395
- │ │ │ ├── extract/ # _zod.def SchemaIR (extractors per type)
396
- │ │ │ └── codegen/ # SchemaIR optimized JS
397
- │ │ ├── cli/ # CLI commands (generate, check, watch)
398
- │ │ └── unplugin/ # Build plugin (Vite/webpack/esbuild/Rollup/Rolldown/rspack/Bun)
399
- │ └── tests/
400
- ├── benchmarks/ # vitest bench (zod v3 vs v4 vs zod-aot vs ajv vs typia)
401
- └── .github/workflows/ # CI + release automation
402
- ```
328
+ | Scenario | Zod v3 | Zod v4 | **Zod AOT** | Typia | AJV | vs Zod v4 |
329
+ |---|---|---|---|---|---|---|
330
+ | simple string | 8.7M | 10.0M | **11.0M** | 11.0M | 11.1M | 1.1x |
331
+ | string (min/max) | 8.3M | 6.0M | **11.1M** | 11.3M | 9.5M | 1.8x |
332
+ | number (int+positive) | 8.2M | 5.8M | **10.9M** | 10.7M | 10.9M | 1.9x |
333
+ | enum | 8.0M | 9.5M | **10.6M** | 10.6M | 10.5M | 1.1x |
334
+ | bigint (min/max) | 8.0M | 6.0M | **10.9M** | — | — | 1.8x |
335
+ | tuple [string, int, bool] | 4.0M | 4.4M | **10.5M** | 10.8M | 10.5M | 2.4x |
336
+ | record\<string, number\> | 2.3M | 1.9M | **5.4M** | 7.5M | 9.4M | 2.8x |
337
+ | set\<string\> (5 items) | 2.7M | 1.6M | **9.8M** | — | — | 6.3x |
338
+ | set\<string\> (20 items) | 1.0M | 475K | **7.7M** | — | — | **16x** |
339
+ | map\<string, number\> (5 entries) | 1.5M | 946K | **8.5M** | — | — | 9.0x |
340
+ | map\<string, number\> (20 entries) | 490K | 238K | **5.2M** | — | — | **22x** |
341
+ | pipe (non-transform) | 6.3M | 3.8M | **10.8M** | — | — | 2.8x |
342
+ | discriminatedUnion (3 variants) | 2.3M | 2.9M | **9.8M** | 10.4M | 5.6M | 3.4x |
343
+ | medium object (valid) | 1.3M | 1.7M | **5.4M** | 7.3M | 5.0M | 3.1x |
344
+ | medium object (invalid) | 351K | 65K | **471K** | 2.1M | 5.6M | 7.3x |
345
+ | large object (10 items) | 82K | 111K | **4.0M** | 4.1M | 834K | **36x** |
346
+ | large object (100 items) | 9.0K | 11.6K | **676K** | 808K | 89K | **58x** |
347
+ | recursive tree (7 nodes) | 424K | 1.5M | **6.4M** | 8.1M | 3.1M | 4.1x |
348
+ | recursive tree (121 nodes) | 24K | 101K | **741K** | 1.4M | 250K | 7.4x |
349
+ | event log (combined) | 260K | 479K | **4.5M** | — | — | 9.4x |
350
+ | partial fallback obj (transform) | 817K | 1.4M | **3.3M** | — | — | 2.3x |
351
+ | partial fallback arr 10 (transform) | 88K | 139K | **841K** | — | — | 6.1x |
352
+ | partial fallback arr 50 (transform) | 18K | 28K | **175K** | — | — | 6.3x |
403
353
 
404
- ## Contributing
354
+ *ops/s, higher is better. "—" = not supported by the library. Measured with `vitest bench` on Apple M-series.*
405
355
 
406
- Contributions are welcome! Please ensure your changes pass all checks before submitting a PR:
356
+ Performance scales with schema complexity. Nested objects and arrays see the biggest gains because zod-aot eliminates per-node traversal overhead. `discriminatedUnion` uses O(1) `switch` dispatch instead of Zod's sequential trial. Partial fallback schemas (containing `transform`/`refine`) still show 2-6x speedups.
407
357
 
408
358
  ```bash
409
- pnpm lint && pnpm -r typecheck && pnpm test
359
+ pnpm bench # run locally
410
360
  ```
411
361
 
362
+ ### Performance Architecture
363
+
364
+ For eligible schemas, zod-aot generates a **two-phase validator**:
365
+
366
+ 1. **Fast Path** — A single `&&` expression chain that validates the entire input with zero allocations. Valid input returns immediately.
367
+ 2. **Slow Path** — Error-collecting validation that only runs when the Fast Path fails.
368
+
369
+ Additional optimizations: check ordering (cheap checks first), pre-compiled regex, Set-based enum lookups, small enum inlining (`===` for 1-3 values).
370
+
371
+ Run `npx zod-aot check --json` to see which schemas qualify for Fast Path.
372
+
412
373
  ## License
413
374
 
414
375
  [MIT](LICENSE)