zod-aot 0.13.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.
- package/README.md +239 -338
- package/dist/cli/commands/doctor.d.ts +8 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +136 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/fallback.d.ts +7 -0
- package/dist/cli/fallback.d.ts.map +1 -0
- package/dist/cli/fallback.js +52 -0
- package/dist/cli/fallback.js.map +1 -0
- package/dist/core/iife.d.ts.map +1 -1
- package/dist/core/iife.js +4 -4
- package/dist/core/iife.js.map +1 -1
- package/dist/discovery.d.ts +10 -3
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +18 -2
- package/dist/discovery.js.map +1 -1
- package/dist/unplugin/index.d.ts.map +1 -1
- package/dist/unplugin/index.js +1 -0
- package/dist/unplugin/index.js.map +1 -1
- package/dist/unplugin/transform.d.ts +13 -0
- package/dist/unplugin/transform.d.ts.map +1 -1
- package/dist/unplugin/transform.js +101 -13
- package/dist/unplugin/transform.js.map +1 -1
- package/dist/unplugin/types.d.ts +10 -0
- package/dist/unplugin/types.d.ts.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -7,467 +7,368 @@
|
|
|
7
7
|
[](https://www.npmjs.com/package/zod-aot)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Keep your existing Zod schemas. Get **2-64x faster** validation. No code changes required.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
+
## Install
|
|
28
35
|
|
|
29
36
|
```bash
|
|
30
|
-
|
|
37
|
+
npm install zod-aot zod@^4
|
|
31
38
|
```
|
|
32
39
|
|
|
33
|
-
|
|
40
|
+
| Runtime | Version |
|
|
41
|
+
|---------|---------|
|
|
42
|
+
| Node.js | 20+ |
|
|
43
|
+
| Bun | 1.3+ |
|
|
44
|
+
| Deno | 2.0+ |
|
|
34
45
|
|
|
35
|
-
|
|
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
|
-
|
|
48
|
+
There are three ways to use zod-aot. Choose the one that fits your project.
|
|
62
49
|
|
|
63
|
-
|
|
50
|
+
### 1. autoDiscover (Recommended)
|
|
64
51
|
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
+
```typescript
|
|
57
|
+
import zodAot from "zod-aot/vite";
|
|
74
58
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
npm install zod@^4
|
|
59
|
+
export default defineConfig({
|
|
60
|
+
plugins: [zodAot({ autoDiscover: true })],
|
|
61
|
+
});
|
|
79
62
|
```
|
|
80
63
|
|
|
81
|
-
|
|
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
|
|
91
|
-
name: z.string().min(
|
|
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
|
-
|
|
73
|
+
age: z.number().int().min(0).max(150),
|
|
74
|
+
role: z.enum(["admin", "editor", "viewer"]),
|
|
95
75
|
});
|
|
96
76
|
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
88
|
+
**Use them as usual:**
|
|
102
89
|
|
|
103
90
|
```typescript
|
|
104
|
-
|
|
105
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
103
|
+
### 2. compile() (Explicit)
|
|
112
104
|
|
|
113
|
-
|
|
105
|
+
If you prefer explicit opt-in, wrap specific schemas with `compile()`:
|
|
114
106
|
|
|
115
107
|
```typescript
|
|
116
|
-
|
|
117
|
-
import
|
|
108
|
+
import { z } from "zod";
|
|
109
|
+
import { compile } from "zod-aot";
|
|
118
110
|
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
128
|
+
Generate optimized validation files from the command line:
|
|
127
129
|
|
|
128
130
|
```bash
|
|
129
|
-
#
|
|
131
|
+
# Single file
|
|
130
132
|
npx zod-aot generate src/schemas.ts -o src/schemas.compiled.ts
|
|
131
133
|
|
|
132
|
-
#
|
|
134
|
+
# Directory
|
|
133
135
|
npx zod-aot generate src/ -o src/compiled/
|
|
134
136
|
|
|
135
|
-
# Watch mode
|
|
137
|
+
# Watch mode
|
|
136
138
|
npx zod-aot generate src/ --watch
|
|
139
|
+
```
|
|
137
140
|
|
|
138
|
-
|
|
139
|
-
npx zod-aot check src/schemas.ts
|
|
141
|
+
## Build Plugin
|
|
140
142
|
|
|
141
|
-
|
|
142
|
-
npx zod-aot check src/schemas.ts --json
|
|
143
|
+
### Supported Build Tools
|
|
143
144
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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"` |
|
|
147
154
|
|
|
148
|
-
|
|
155
|
+
### Options
|
|
149
156
|
|
|
150
|
-
|
|
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 |
|
|
151
164
|
|
|
152
165
|
```typescript
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const zodAot = require("zod-aot/webpack");
|
|
159
|
-
module.exports = { plugins: [zodAot()] };
|
|
166
|
+
zodAot({
|
|
167
|
+
autoDiscover: true,
|
|
168
|
+
include: ["src/schemas"],
|
|
169
|
+
verbose: true,
|
|
170
|
+
})
|
|
160
171
|
```
|
|
161
172
|
|
|
162
|
-
###
|
|
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:
|
|
163
178
|
|
|
164
179
|
```typescript
|
|
165
180
|
zodAot({
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
verbose: true, // log per-schema compilation status and build summary
|
|
181
|
+
autoDiscover: true,
|
|
182
|
+
include: ["src/schemas", "src/validators"],
|
|
169
183
|
})
|
|
170
184
|
```
|
|
171
185
|
|
|
172
|
-
|
|
173
|
-
- Runs at build time (`enforce: "pre"`)
|
|
174
|
-
- Replaces `compile(Schema)` with optimized IIFE inline validators
|
|
175
|
-
- Adds `/* @__PURE__ */` annotations for tree-shaking
|
|
176
|
-
- Supports HMR in development
|
|
186
|
+
### autoDiscover vs compile()
|
|
177
187
|
|
|
178
|
-
|
|
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 |
|
|
179
195
|
|
|
180
|
-
|
|
196
|
+
## Framework Examples
|
|
181
197
|
|
|
182
|
-
###
|
|
198
|
+
### tRPC
|
|
183
199
|
|
|
184
200
|
```typescript
|
|
185
|
-
|
|
186
|
-
import { zValidator } from "@hono/zod-validator";
|
|
201
|
+
// src/schemas.ts
|
|
187
202
|
import { z } from "zod";
|
|
188
|
-
import { compile } from "zod-aot";
|
|
189
203
|
|
|
190
|
-
const
|
|
191
|
-
name: z.string().min(
|
|
192
|
-
age: z.number().int().positive(),
|
|
204
|
+
export const CreateUserSchema = z.object({
|
|
205
|
+
name: z.string().min(1).max(100),
|
|
193
206
|
email: z.email(),
|
|
207
|
+
age: z.number().int().min(0).max(150),
|
|
208
|
+
});
|
|
209
|
+
|
|
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)),
|
|
194
217
|
});
|
|
218
|
+
```
|
|
195
219
|
|
|
196
|
-
|
|
197
|
-
|
|
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";
|
|
198
228
|
|
|
199
229
|
const app = new Hono();
|
|
200
230
|
|
|
201
|
-
app.post("/users", zValidator("json",
|
|
231
|
+
app.post("/users", zValidator("json", UserSchema), (c) => {
|
|
202
232
|
const user = c.req.valid("json");
|
|
203
233
|
return c.json(user);
|
|
204
234
|
});
|
|
205
235
|
```
|
|
206
236
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
## Schema Diagnostics (`check`)
|
|
210
|
-
|
|
211
|
-
The `check` command analyzes schemas for compilation coverage, Fast Path eligibility, and actionable hints — without generating code.
|
|
237
|
+
### React Hook Form
|
|
212
238
|
|
|
213
|
-
```
|
|
214
|
-
|
|
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
|
+
}
|
|
215
250
|
```
|
|
216
251
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
- **Tree view** — hierarchical visualization of schema structure with compile/fallback status per node
|
|
220
|
-
- **Coverage** — percentage of schema nodes that are compiled (e.g. `85% compiled (17/20 nodes)`)
|
|
221
|
-
- **Fast Path eligibility** — whether the schema qualifies for two-phase validation, with the specific blocker if ineligible
|
|
222
|
-
- **Hints** — actionable suggestions (e.g. "Replace `.refine()` with built-in checks")
|
|
252
|
+
### Any Standard Schema Consumer
|
|
223
253
|
|
|
224
|
-
|
|
254
|
+
zod-aot compiled schemas implement [Standard Schema](https://standardschema.dev) via prototype chain. Any library that accepts Standard Schema validators works automatically.
|
|
225
255
|
|
|
226
|
-
|
|
227
|
-
|---|---|
|
|
228
|
-
| `--json` | Output structured JSON (for CI/CD integration) |
|
|
229
|
-
| `--fail-under <pct>` | Exit with code 1 if any schema's coverage is below the threshold |
|
|
230
|
-
| `--no-color` | Disable colored output |
|
|
256
|
+
## Schema Diagnostics
|
|
231
257
|
|
|
232
|
-
|
|
258
|
+
Analyze your schemas before compiling — check coverage, Fast Path eligibility, and get actionable hints:
|
|
233
259
|
|
|
234
260
|
```bash
|
|
235
|
-
npx zod-aot check src/schemas.ts
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
```json
|
|
239
|
-
[
|
|
240
|
-
{
|
|
241
|
-
"file": "src/schemas.ts",
|
|
242
|
-
"schemas": [
|
|
243
|
-
{
|
|
244
|
-
"exportName": "validateUser",
|
|
245
|
-
"coverage": { "total": 5, "compilable": 5, "percent": 100 },
|
|
246
|
-
"fastPath": { "eligible": true },
|
|
247
|
-
"fallbacks": []
|
|
248
|
-
}
|
|
249
|
-
]
|
|
250
|
-
}
|
|
251
|
-
]
|
|
261
|
+
npx zod-aot check src/schemas.ts
|
|
252
262
|
```
|
|
253
263
|
|
|
254
|
-
|
|
264
|
+
Output:
|
|
255
265
|
|
|
256
|
-
```bash
|
|
257
|
-
npx zod-aot check src/schemas.ts --json --fail-under 80
|
|
258
266
|
```
|
|
267
|
+
src/schemas.ts
|
|
259
268
|
|
|
260
|
-
|
|
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
|
|
261
274
|
|
|
262
|
-
|
|
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
|
|
263
280
|
|
|
281
|
+
Summary: 2 schemas | 1 Fast Path, 1 Slow Path | 8/9 nodes (88.9%)
|
|
264
282
|
```
|
|
265
|
-
Zod Schema
|
|
266
|
-
│
|
|
267
|
-
▼
|
|
268
|
-
┌─────────────────────┐
|
|
269
|
-
│ Extractor │ Traverse _zod.def recursively
|
|
270
|
-
│ extractSchema() │ → produce JSON-serializable IR
|
|
271
|
-
└─────────┬───────────┘
|
|
272
|
-
│ SchemaIR
|
|
273
|
-
▼
|
|
274
|
-
┌─────────────────────┐
|
|
275
|
-
│ CodeGen │ IR → optimized JS with:
|
|
276
|
-
│ generateValidator()│ • inline type checks
|
|
277
|
-
└─────────┬───────────┘ • pre-compiled RegExps
|
|
278
|
-
│ • Set-based enum lookups
|
|
279
|
-
▼ • early returns
|
|
280
|
-
Optimized JS function
|
|
281
|
-
(no runtime dependencies)
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
### Why Runtime Extraction (not static analysis)
|
|
285
|
-
|
|
286
|
-
- Zod v4's `_zod.def` is JSON-serializable — perfect for IR extraction
|
|
287
|
-
- Static AST analysis cannot handle dynamic schemas (variable references, function calls, spread operators)
|
|
288
|
-
- `_zod.bag` contains aggregated metadata from checks (minimum, maximum, regex patterns, etc.)
|
|
289
|
-
- Reliable detection of `transform`/`refine` for automatic fallback
|
|
290
|
-
|
|
291
|
-
### Generated Code Example
|
|
292
283
|
|
|
293
|
-
|
|
284
|
+
### CI Integration
|
|
294
285
|
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
role: z.enum(["admin", "user"]),
|
|
299
|
-
})
|
|
300
|
-
```
|
|
286
|
+
```bash
|
|
287
|
+
# JSON output
|
|
288
|
+
npx zod-aot check src/schemas.ts --json
|
|
301
289
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
```javascript
|
|
305
|
-
/* zod-aot */
|
|
306
|
-
var __set_0 = new Set(["admin", "user"]);
|
|
307
|
-
|
|
308
|
-
function safeParse_validate(input) {
|
|
309
|
-
var __issues = [];
|
|
310
|
-
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
311
|
-
__issues.push({ code: "invalid_type", expected: "object", path: [] });
|
|
312
|
-
} else {
|
|
313
|
-
// name: string().min(3).max(50)
|
|
314
|
-
if (typeof input["name"] !== "string") {
|
|
315
|
-
__issues.push({ code: "invalid_type", expected: "string", path: ["name"] });
|
|
316
|
-
} else {
|
|
317
|
-
if (input["name"].length < 3)
|
|
318
|
-
__issues.push({ code: "too_small", minimum: 3, path: ["name"] });
|
|
319
|
-
if (input["name"].length > 50)
|
|
320
|
-
__issues.push({ code: "too_big", maximum: 50, path: ["name"] });
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// role: enum(["admin", "user"])
|
|
324
|
-
if (!__set_0.has(input["role"])) {
|
|
325
|
-
__issues.push({ code: "invalid_enum_value", path: ["role"] });
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
if (__issues.length > 0) return { success: false, error: { issues: __issues } };
|
|
329
|
-
return { success: true, data: input };
|
|
330
|
-
}
|
|
290
|
+
# Fail if any schema below 80% coverage
|
|
291
|
+
npx zod-aot check src/schemas.ts --json --fail-under 80
|
|
331
292
|
```
|
|
332
293
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
- **Early type checks** — nested checks only run if the type is correct
|
|
339
|
-
- **Minimal allocations** — issues array is only created once per call
|
|
340
|
-
|
|
341
|
-
## API Reference
|
|
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 |
|
|
342
299
|
|
|
343
|
-
|
|
300
|
+
## What Gets Compiled
|
|
344
301
|
|
|
345
|
-
|
|
302
|
+
### Fully Compiled (3-64x faster)
|
|
346
303
|
|
|
347
|
-
|
|
348
|
-
import { z } from "zod";
|
|
349
|
-
import { compile } from "zod-aot";
|
|
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)
|
|
350
305
|
|
|
351
|
-
|
|
352
|
-
name: z.string().min(3),
|
|
353
|
-
age: z.number().int().positive(),
|
|
354
|
-
}));
|
|
355
|
-
```
|
|
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.
|
|
356
307
|
|
|
357
|
-
###
|
|
308
|
+
### Falls Back to Zod (Still Works, Not Faster)
|
|
358
309
|
|
|
359
|
-
|
|
310
|
+
These types contain JavaScript closures that cannot be compiled to static code:
|
|
360
311
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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 |
|
|
368
319
|
|
|
369
|
-
|
|
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.
|
|
370
321
|
|
|
371
|
-
|
|
372
|
-
|---|---|
|
|
373
|
-
| `string` | `min`, `max`, `length`, `email`, `url`, `uuid`, `regex`, `includes`, `startsWith`, `endsWith` |
|
|
374
|
-
| `number` | `int`, `positive`, `negative`, `nonnegative`, `nonpositive`, `min`, `max`, `multipleOf`, `int32`, `uint32`, `float32`, `float64` |
|
|
375
|
-
| `bigint` | `min`, `max`, `positive`, `negative`, `nonnegative`, `nonpositive`, `multipleOf` |
|
|
376
|
-
| `boolean` | — |
|
|
377
|
-
| `null` | — |
|
|
378
|
-
| `undefined` | — |
|
|
379
|
-
| `any` | — (always passes) |
|
|
380
|
-
| `unknown` | — (always passes) |
|
|
381
|
-
| `literal` | single value, multi-value |
|
|
382
|
-
| `enum` | string enum values |
|
|
383
|
-
| `date` | `min`, `max` (timestamp comparison) |
|
|
384
|
-
| `object` | nested objects, mixed property types |
|
|
385
|
-
| `array` | `min`, `max`, `length`, element validation |
|
|
386
|
-
| `tuple` | per-element types, optional rest element |
|
|
387
|
-
| `record` | key and value type validation |
|
|
388
|
-
| `set` | `min`, `max` (size), element validation |
|
|
389
|
-
| `map` | key and value type validation |
|
|
390
|
-
| `union` | sequential trial of all options |
|
|
391
|
-
| `discriminatedUnion` | O(1) `switch` dispatch on discriminator field |
|
|
392
|
-
| `intersection` | validates both left and right schemas |
|
|
393
|
-
| `pipe` (non-transform) | sequential in→out validation |
|
|
394
|
-
| `optional` | wraps any supported type |
|
|
395
|
-
| `nullable` | wraps any supported type |
|
|
396
|
-
| `readonly` | validates inner type (TS-only concept) |
|
|
397
|
-
| `default` | replaces `undefined` with default value, then validates inner |
|
|
398
|
-
| `symbol` | — (typeof check) |
|
|
399
|
-
| `void` | — (accepts `undefined`) |
|
|
400
|
-
| `nan` | — (Number.isNaN check) |
|
|
401
|
-
| `never` | — (always fails) |
|
|
402
|
-
| `lazy` (self-recursive) | cycle detection → self-recursive codegen |
|
|
403
|
-
|
|
404
|
-
### Automatic Fallback to Zod
|
|
405
|
-
|
|
406
|
-
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`:
|
|
407
|
-
|
|
408
|
-
- `transform` — runtime data transformation
|
|
409
|
-
- `refine` / `superRefine` — custom validation with closures
|
|
410
|
-
- `custom` — arbitrary validation logic
|
|
411
|
-
- `preprocess` — input preprocessing
|
|
412
|
-
- `lazy` (non-recursive) — deferred schema resolution where inner type cannot be resolved
|
|
413
|
-
|
|
414
|
-
#### Partial Fallback
|
|
415
|
-
|
|
416
|
-
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.
|
|
417
|
-
|
|
418
|
-
> **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.
|
|
419
|
-
|
|
420
|
-
## Development
|
|
322
|
+
**Tip:** Run `npx zod-aot check` to see exactly which parts of your schemas are compiled and which fall back.
|
|
421
323
|
|
|
422
|
-
|
|
423
|
-
# Install dependencies
|
|
424
|
-
pnpm install
|
|
324
|
+
## Benchmarks
|
|
425
325
|
|
|
426
|
-
|
|
427
|
-
pnpm test
|
|
326
|
+
5-way comparison: **Zod v3** vs **Zod v4** vs **Zod AOT** vs **[Typia](https://typia.io/)** vs **[AJV](https://ajv.js.org/)**
|
|
428
327
|
|
|
429
|
-
|
|
430
|
-
|
|
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 |
|
|
431
353
|
|
|
432
|
-
|
|
433
|
-
pnpm lint
|
|
354
|
+
*ops/s, higher is better. "—" = not supported by the library. Measured with `vitest bench` on Apple M-series.*
|
|
434
355
|
|
|
435
|
-
|
|
436
|
-
pnpm -r typecheck
|
|
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.
|
|
437
357
|
|
|
438
|
-
|
|
439
|
-
pnpm
|
|
358
|
+
```bash
|
|
359
|
+
pnpm bench # run locally
|
|
440
360
|
```
|
|
441
361
|
|
|
442
|
-
###
|
|
362
|
+
### Performance Architecture
|
|
443
363
|
|
|
444
|
-
|
|
445
|
-
zod-aot/
|
|
446
|
-
├── packages/zod-aot/ # Main npm package
|
|
447
|
-
│ ├── src/
|
|
448
|
-
│ │ ├── index.ts # Public API exports (zod-aot)
|
|
449
|
-
│ │ ├── discovery.ts # Schema discovery (shared by CLI & unplugin)
|
|
450
|
-
│ │ ├── loader.ts # Runtime-aware file loader
|
|
451
|
-
│ │ ├── core/ # Pure logic (no CLI/unplugin deps)
|
|
452
|
-
│ │ │ ├── types.ts # SchemaIR, CompiledSchema, CheckIR
|
|
453
|
-
│ │ │ ├── compile.ts # compile() marker + isCompiledSchema()
|
|
454
|
-
│ │ │ ├── runtime.ts # createFallback (dev-time)
|
|
455
|
-
│ │ │ ├── extract/ # _zod.def → SchemaIR (extractors per type)
|
|
456
|
-
│ │ │ └── codegen/ # SchemaIR → optimized JS
|
|
457
|
-
│ │ ├── cli/ # CLI commands (generate, check, watch)
|
|
458
|
-
│ │ └── unplugin/ # Build plugin (Vite/webpack/esbuild/Rollup/Rolldown/rspack/Bun)
|
|
459
|
-
│ └── tests/
|
|
460
|
-
├── benchmarks/ # vitest bench (zod v3 vs v4 vs zod-aot vs ajv vs typia)
|
|
461
|
-
└── .github/workflows/ # CI + release automation
|
|
462
|
-
```
|
|
364
|
+
For eligible schemas, zod-aot generates a **two-phase validator**:
|
|
463
365
|
|
|
464
|
-
|
|
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.
|
|
465
368
|
|
|
466
|
-
|
|
369
|
+
Additional optimizations: check ordering (cheap checks first), pre-compiled regex, Set-based enum lookups, small enum inlining (`===` for 1-3 values).
|
|
467
370
|
|
|
468
|
-
|
|
469
|
-
pnpm lint && pnpm -r typecheck && pnpm test
|
|
470
|
-
```
|
|
371
|
+
Run `npx zod-aot check --json` to see which schemas qualify for Fast Path.
|
|
471
372
|
|
|
472
373
|
## License
|
|
473
374
|
|