@spartaficial/openapi-to-zod 0.0.0-stage → 1.0.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/LICENSE +125 -0
- package/README.md +1814 -2
- package/dist/cli.cjs +7 -0
- package/dist/cli.d.cts +6 -0
- package/dist/cli.d.mts +6 -0
- package/dist/cli.mjs +7 -0
- package/dist/index.cjs +6 -0
- package/dist/index.d.cts +698 -0
- package/dist/index.d.mts +698 -0
- package/dist/index.mjs +6 -0
- package/dist/openapi-generator-BFjiKlNv.mjs +6 -0
- package/dist/openapi-generator-DxvdmzZQ.cjs +6 -0
- package/package.json +85 -5
package/README.md
CHANGED
|
@@ -1,3 +1,1815 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @spartaficial/openapi-to-zod
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Transform OpenAPI YAML specifications into Zod v4 compliant schemas with full TypeScript support.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ **Zod v4 Compatible**: Uses latest Zod features, no deprecated methods
|
|
8
|
+
- 📝 **TypeScript Types**: Automatically generates TypeScript types from schemas using `z.infer`
|
|
9
|
+
- 🎯 **Zod Enums**: Creates Zod enum schemas from OpenAPI enums
|
|
10
|
+
- 🔧 **Flexible Modes**: Strict (default), normal, or loose validation
|
|
11
|
+
- 📐 **Format Support**: Full support for string formats (uuid, email, url, date, etc.)
|
|
12
|
+
- 🎨 **camelCase Schemas**: Schemas follow camelCase naming with Schema suffix
|
|
13
|
+
- ❓ **Optional Properties**: Uses `.optional()` for optional properties instead of `.partial()`
|
|
14
|
+
- 🔀 **Discriminated Unions**: Automatic `z.discriminatedUnion()` for oneOf/anyOf with discriminators
|
|
15
|
+
- 🔐 **readOnly/writeOnly**: Generate separate request/response schemas
|
|
16
|
+
- 📋 **Constraint Support**: multipleOf, additionalProperties, array constraints, min/maxProperties, and more
|
|
17
|
+
- 💬 **Runtime Descriptions**: Optional `.describe()` calls for better error messages
|
|
18
|
+
- 🏷️ **Schema Naming**: Add custom prefixes and suffixes to schema names
|
|
19
|
+
- 📊 **Statistics**: Optional generation statistics in output files
|
|
20
|
+
- ❗ **Better Errors**: Clear error messages with file paths and line numbers
|
|
21
|
+
- 🎭 **Tuple Validation**: OpenAPI 3.1 `prefixItems` support with `.tuple()` and `.rest()`
|
|
22
|
+
- 🔗 **Smart AllOf**: Uses `.extend()` for objects (Zod v4), `.and()` for primitives
|
|
23
|
+
- 🎯 **Literal Types**: `const` keyword support with `z.literal()`
|
|
24
|
+
- 🔢 **Exclusive Bounds**: `exclusiveMinimum`/`exclusiveMaximum` with `.gt()`/`.lt()`
|
|
25
|
+
- 🎨 **Unique Arrays**: `uniqueItems` validation with Set-based checking
|
|
26
|
+
- 📛 **Deprecation**: `@deprecated` JSDoc annotations for deprecated schemas
|
|
27
|
+
- 🏷️ **Metadata**: `title` and `examples` in JSDoc comments
|
|
28
|
+
- 🔒 **OpenAPI 3.1 Nullable**: Type array syntax `type: ["string", "null"]` support
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install @spartaficial/openapi-to-zod
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
> Commercial software. Generation requires a license key — see [Licensing](#licensing).
|
|
37
|
+
|
|
38
|
+
## CLI Usage
|
|
39
|
+
|
|
40
|
+
### Quick Start
|
|
41
|
+
|
|
42
|
+
#### 1. Initialize Configuration
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npx @spartaficial/openapi-to-zod init
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
This interactive command will:
|
|
49
|
+
|
|
50
|
+
- Prompt for your OpenAPI spec path
|
|
51
|
+
- Prompt for output file path
|
|
52
|
+
- Ask if you want to include commonly-used defaults
|
|
53
|
+
- Generate a config file (`openapi-to-zod.config.ts` or `.json`)
|
|
54
|
+
|
|
55
|
+
#### 2. Generate Schemas
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npx @spartaficial/openapi-to-zod
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The tool will auto-discover your config file and generate schemas.
|
|
62
|
+
|
|
63
|
+
### Configuration
|
|
64
|
+
|
|
65
|
+
> Every spec names an `input` and an `outputTypes`. A spec that says `output` instead is rejected
|
|
66
|
+
> with a hint to rename it.
|
|
67
|
+
|
|
68
|
+
#### TypeScript Config (Recommended)
|
|
69
|
+
|
|
70
|
+
**Minimal:**
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { defineConfig } from "@spartaficial/openapi-to-zod";
|
|
74
|
+
|
|
75
|
+
export default defineConfig({
|
|
76
|
+
specs: [
|
|
77
|
+
{
|
|
78
|
+
input: "openapi.yaml",
|
|
79
|
+
outputTypes: "src/schemas.ts",
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**With Commonly-Used Defaults:**
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import { defineConfig } from "@spartaficial/openapi-to-zod";
|
|
89
|
+
|
|
90
|
+
export default defineConfig({
|
|
91
|
+
defaults: {
|
|
92
|
+
mode: "strict", // Strictest validation
|
|
93
|
+
includeDescriptions: true, // Useful JSDoc comments
|
|
94
|
+
showStats: false, // Cleaner output
|
|
95
|
+
},
|
|
96
|
+
specs: [
|
|
97
|
+
{
|
|
98
|
+
input: "openapi.yaml",
|
|
99
|
+
outputTypes: "src/schemas.ts",
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Multi-Spec with Custom Options:**
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import { defineConfig } from "@spartaficial/openapi-to-zod";
|
|
109
|
+
|
|
110
|
+
export default defineConfig({
|
|
111
|
+
defaults: {
|
|
112
|
+
mode: "strict",
|
|
113
|
+
includeDescriptions: true,
|
|
114
|
+
},
|
|
115
|
+
specs: [
|
|
116
|
+
{
|
|
117
|
+
name: "api-v1",
|
|
118
|
+
input: "specs/api-v1.yaml",
|
|
119
|
+
outputTypes: "src/schemas/v1.ts",
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: "api-v2",
|
|
123
|
+
input: "specs/api-v2.yaml",
|
|
124
|
+
outputTypes: "src/schemas/v2.ts",
|
|
125
|
+
mode: "normal", // Override default
|
|
126
|
+
prefix: "v2",
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
executionMode: "parallel", // Process specs in parallel (default)
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
#### JSON Config
|
|
134
|
+
|
|
135
|
+
**openapi-to-zod.config.json:**
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"defaults": {
|
|
140
|
+
"mode": "strict",
|
|
141
|
+
"includeDescriptions": true
|
|
142
|
+
},
|
|
143
|
+
"specs": [
|
|
144
|
+
{
|
|
145
|
+
"input": "openapi.yaml",
|
|
146
|
+
"outputTypes": "src/schemas.ts"
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### CLI Reference
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
openapi-to-zod [options]
|
|
156
|
+
|
|
157
|
+
Options:
|
|
158
|
+
-c, --config <path> Path to config file (optional if using auto-discovery)
|
|
159
|
+
-V, --version Output version number
|
|
160
|
+
-h, --help Display help
|
|
161
|
+
|
|
162
|
+
Commands:
|
|
163
|
+
init Initialize a new config file
|
|
164
|
+
|
|
165
|
+
Examples:
|
|
166
|
+
# Create config
|
|
167
|
+
$ openapi-to-zod init
|
|
168
|
+
|
|
169
|
+
# Generate (auto-discover config)
|
|
170
|
+
$ openapi-to-zod
|
|
171
|
+
|
|
172
|
+
# Generate with custom config path
|
|
173
|
+
$ openapi-to-zod --config custom.config.ts
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Configuration Options
|
|
177
|
+
|
|
178
|
+
| Option | Type | Description |
|
|
179
|
+
| --------------- | ------------------------------ | ---------------------------------------------------------------- |
|
|
180
|
+
| `defaults` | `object` | Global options applied to all specs (can be overridden per-spec) |
|
|
181
|
+
| `specs` | `array` | Array of spec configurations (required, minimum 1) |
|
|
182
|
+
| `executionMode` | `"parallel"` \| `"sequential"` | How to process specs (default: `"parallel"`) |
|
|
183
|
+
|
|
184
|
+
**Per-Spec Options:**
|
|
185
|
+
|
|
186
|
+
| Spec Option | Type | Description |
|
|
187
|
+
| --------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
188
|
+
| `name` | `string` | Optional identifier for logging |
|
|
189
|
+
| `input` | `string` | Input OpenAPI YAML file path (required) |
|
|
190
|
+
| `outputTypes` | `string` | Output TypeScript file path (required) |
|
|
191
|
+
| `outputZodSchemas` | `string` | Separate output path for Zod schemas (recommended for circular references, see below) |
|
|
192
|
+
| `mode` | `"strict"` \| `"normal"` \| `"loose"` | Validation mode for top-level schemas (default: `"strict"`) |
|
|
193
|
+
| `emptyObjectBehavior` | `"strict"` \| `"loose"` \| `"record"` | How to handle empty objects (default: `"loose"`) |
|
|
194
|
+
| `includeDescriptions` | `boolean` | Include JSDoc comments |
|
|
195
|
+
| `useDescribe` | `boolean` | Add `.describe()` calls |
|
|
196
|
+
| `defaultNullable` | `boolean` | Treat properties as nullable by default when not explicitly specified (default: `false`) |
|
|
197
|
+
| `schemaType` | `"all"` \| `"request"` \| `"response"` | Schema filtering |
|
|
198
|
+
| `prefix` | `string` | Prefix for schema names |
|
|
199
|
+
| `suffix` | `string` | Suffix for schema names |
|
|
200
|
+
| `stripSchemaPrefix` | `string` | Strip prefix from schema names before generating using glob patterns (e.g., `"Company.Models."` or `"*.Models."`) |
|
|
201
|
+
| `useOperationId` | `boolean` | Use operationId for operation-derived query/header schema names when available (default: `true`) |
|
|
202
|
+
| `showStats` | `boolean` | Include generation statistics |
|
|
203
|
+
| `request` | `object` | Request-specific options (mode, includeDescriptions, useDescribe, defaultNullable, emptyObjectBehavior) |
|
|
204
|
+
| `response` | `object` | Response-specific options (mode, includeDescriptions, useDescribe, defaultNullable, emptyObjectBehavior) |
|
|
205
|
+
| `operationFilters` | `object` | Filter operations by tags, paths, methods, etc. (see below) |
|
|
206
|
+
| `refMappings` | `Record<string, string>` | Redirect remote `$ref` URLs to local files by prefix (see below) |
|
|
207
|
+
|
|
208
|
+
If `outputTypes` and `output` are both set with different values, configuration validation fails.
|
|
209
|
+
|
|
210
|
+
#### Operation Filters
|
|
211
|
+
|
|
212
|
+
Filter which operations to include/exclude during schema generation. Useful for generating separate schemas for different API subsets.
|
|
213
|
+
|
|
214
|
+
| Filter | Type | Description |
|
|
215
|
+
| --------------------- | ---------- | ------------------------------------------------------------------ |
|
|
216
|
+
| `includeTags` | `string[]` | Include only operations with these tags |
|
|
217
|
+
| `excludeTags` | `string[]` | Exclude operations with these tags |
|
|
218
|
+
| `includePaths` | `string[]` | Include only these paths (supports glob patterns like `/users/**`) |
|
|
219
|
+
| `excludePaths` | `string[]` | Exclude these paths (supports glob patterns) |
|
|
220
|
+
| `includeMethods` | `string[]` | Include only these HTTP methods (`get`, `post`, etc.) |
|
|
221
|
+
| `excludeMethods` | `string[]` | Exclude these HTTP methods |
|
|
222
|
+
| `includeOperationIds` | `string[]` | Include only these operationIds (supports glob patterns) |
|
|
223
|
+
| `excludeOperationIds` | `string[]` | Exclude these operationIds (supports glob patterns) |
|
|
224
|
+
| `excludeDeprecated` | `boolean` | Exclude deprecated operations |
|
|
225
|
+
|
|
226
|
+
**Example:**
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
export default defineConfig({
|
|
230
|
+
specs: [
|
|
231
|
+
{
|
|
232
|
+
input: "openapi.yaml",
|
|
233
|
+
outputTypes: "schemas.ts",
|
|
234
|
+
operationFilters: {
|
|
235
|
+
includeTags: ["public"], // Only public endpoints
|
|
236
|
+
excludeDeprecated: true, // Skip deprecated operations
|
|
237
|
+
excludePaths: ["/internal/**"], // Exclude internal paths
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
});
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Batch Execution
|
|
245
|
+
|
|
246
|
+
**Parallel Mode** (default):
|
|
247
|
+
|
|
248
|
+
- Processes all specs concurrently
|
|
249
|
+
- Faster for multiple specs
|
|
250
|
+
- Recommended for most use cases
|
|
251
|
+
- Live progress shows all specs processing simultaneously
|
|
252
|
+
|
|
253
|
+
**Sequential Mode**:
|
|
254
|
+
|
|
255
|
+
- Processes specs one at a time
|
|
256
|
+
- Useful for resource-constrained environments
|
|
257
|
+
- Easier to debug issues
|
|
258
|
+
- Live progress shows specs processing in order
|
|
259
|
+
|
|
260
|
+
Both modes:
|
|
261
|
+
|
|
262
|
+
- Continue processing even if some specs fail
|
|
263
|
+
- Collect all errors and report at the end
|
|
264
|
+
- Exit with code 1 if any spec fails
|
|
265
|
+
- Show live progress updates to stderr
|
|
266
|
+
|
|
267
|
+
Example output:
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
Executing 3 spec(s) in parallel...
|
|
271
|
+
|
|
272
|
+
Processing [1/3] api-v1...
|
|
273
|
+
✓ Successfully generated src/schemas/v1.ts
|
|
274
|
+
Processing [2/3] api-v2...
|
|
275
|
+
✓ Successfully generated src/schemas/v2.ts
|
|
276
|
+
Processing [3/3] admin-api...
|
|
277
|
+
✗ Failed to generate src/schemas/admin.ts: Invalid YAML syntax
|
|
278
|
+
|
|
279
|
+
==================================================
|
|
280
|
+
Batch Execution Summary
|
|
281
|
+
==================================================
|
|
282
|
+
Total specs: 3
|
|
283
|
+
Successful: 2
|
|
284
|
+
Failed: 1
|
|
285
|
+
|
|
286
|
+
Failed specs:
|
|
287
|
+
✗ admin-api
|
|
288
|
+
Error: Failed to parse OpenAPI YAML file at specs/admin.yaml: Invalid YAML syntax
|
|
289
|
+
==================================================
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Programmatic Usage
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
import { OpenApiGenerator } from "@spartaficial/openapi-to-zod";
|
|
296
|
+
|
|
297
|
+
const generator = new OpenApiGenerator({
|
|
298
|
+
input: "path/to/openapi.yaml",
|
|
299
|
+
outputTypes: "path/to/schemas.ts",
|
|
300
|
+
mode: "strict", // 'strict' (default) | 'normal' | 'loose'
|
|
301
|
+
includeDescriptions: true,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Generate and write to file
|
|
305
|
+
generator.generate();
|
|
306
|
+
|
|
307
|
+
// Or generate as string
|
|
308
|
+
const code = generator.generateString();
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## Validation Modes
|
|
312
|
+
|
|
313
|
+
### Strict Mode (default)
|
|
314
|
+
|
|
315
|
+
Uses `z.strictObject()` which rejects additional properties:
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
const userSchema = z.strictObject({
|
|
319
|
+
id: z.uuid(),
|
|
320
|
+
name: z.string(),
|
|
321
|
+
});
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
The default is `"strict"` so that a payload property the spec does not describe fails validation
|
|
325
|
+
instead of passing unnoticed. Set `mode: "normal"` if your API returns undocumented properties.
|
|
326
|
+
|
|
327
|
+
### Normal Mode
|
|
328
|
+
|
|
329
|
+
Uses `z.object()` which allows additional properties:
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
const userSchema = z.object({
|
|
333
|
+
id: z.uuid(),
|
|
334
|
+
name: z.string(),
|
|
335
|
+
});
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Loose Mode
|
|
339
|
+
|
|
340
|
+
Uses `z.looseObject()` which explicitly allows additional properties:
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
const userSchema = z.looseObject({
|
|
344
|
+
id: z.uuid(),
|
|
345
|
+
name: z.string(),
|
|
346
|
+
});
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## Empty Object Behavior
|
|
350
|
+
|
|
351
|
+
When OpenAPI schemas define an object without any properties (e.g., `type: object` with no `properties`), the generator needs to decide how to represent it. The `emptyObjectBehavior` option controls this:
|
|
352
|
+
|
|
353
|
+
### Loose (default)
|
|
354
|
+
|
|
355
|
+
Uses `z.looseObject({})` which allows any additional properties:
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
// OpenAPI: { type: object }
|
|
359
|
+
const metadataSchema = z.looseObject({});
|
|
360
|
+
|
|
361
|
+
// Accepts: {}, { foo: "bar" }, { any: "properties" }
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Strict
|
|
365
|
+
|
|
366
|
+
Uses `z.strictObject({})` which rejects any properties:
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
// OpenAPI: { type: object }
|
|
370
|
+
const emptySchema = z.strictObject({});
|
|
371
|
+
|
|
372
|
+
// Accepts: {}
|
|
373
|
+
// Rejects: { foo: "bar" }
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### Record
|
|
377
|
+
|
|
378
|
+
Uses `z.record(z.string(), z.unknown())` which treats it as an arbitrary key-value map:
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
// OpenAPI: { type: object }
|
|
382
|
+
const mapSchema = z.record(z.string(), z.unknown());
|
|
383
|
+
|
|
384
|
+
// Accepts: {}, { foo: "bar" }, { any: "properties" }
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
> **Note:** The `mode` option controls how top-level schema definitions are wrapped, while `emptyObjectBehavior` controls how nested empty objects (properties without defined structure) are generated. These are independent settings.
|
|
388
|
+
|
|
389
|
+
## Examples
|
|
390
|
+
|
|
391
|
+
### Input OpenAPI YAML
|
|
392
|
+
|
|
393
|
+
```yaml
|
|
394
|
+
components:
|
|
395
|
+
schemas:
|
|
396
|
+
UserStatusEnumOptions:
|
|
397
|
+
type: string
|
|
398
|
+
enum:
|
|
399
|
+
- active
|
|
400
|
+
- inactive
|
|
401
|
+
- pending
|
|
402
|
+
|
|
403
|
+
User:
|
|
404
|
+
type: object
|
|
405
|
+
required:
|
|
406
|
+
- id
|
|
407
|
+
- email
|
|
408
|
+
properties:
|
|
409
|
+
id:
|
|
410
|
+
type: string
|
|
411
|
+
format: uuid
|
|
412
|
+
minLength: 36
|
|
413
|
+
maxLength: 36
|
|
414
|
+
email:
|
|
415
|
+
type: string
|
|
416
|
+
format: email
|
|
417
|
+
maxLength: 255
|
|
418
|
+
name:
|
|
419
|
+
type: string
|
|
420
|
+
minLength: 1
|
|
421
|
+
maxLength: 100
|
|
422
|
+
age:
|
|
423
|
+
type: integer
|
|
424
|
+
minimum: 0
|
|
425
|
+
maximum: 150
|
|
426
|
+
status:
|
|
427
|
+
$ref: "#/components/schemas/UserStatusEnumOptions"
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### Generated Output
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
// Auto-generated by @spartaficial/openapi-to-zod
|
|
434
|
+
// Do not edit this file manually
|
|
435
|
+
|
|
436
|
+
import { z } from "zod";
|
|
437
|
+
|
|
438
|
+
// Enums
|
|
439
|
+
export enum UserStatusEnum {
|
|
440
|
+
Active = "active",
|
|
441
|
+
Inactive = "inactive",
|
|
442
|
+
Pending = "pending",
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Schemas
|
|
446
|
+
export const userStatusEnumOptionsSchema = z.enum(UserStatusEnum);
|
|
447
|
+
|
|
448
|
+
export const userSchema = z.strictObject({
|
|
449
|
+
id: z.uuid().min(36).max(36),
|
|
450
|
+
email: z.email().max(255),
|
|
451
|
+
name: z.string().min(1).max(100).optional(),
|
|
452
|
+
age: z.number().int().gte(0).lte(150).optional(),
|
|
453
|
+
status: userStatusEnumOptionsSchema.optional(),
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// Types
|
|
457
|
+
export type UserStatusEnumOptions = z.infer<typeof userStatusEnumOptionsSchema>;
|
|
458
|
+
export type User = z.infer<typeof userSchema>;
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
## Format Support
|
|
462
|
+
|
|
463
|
+
The generator supports all OpenAPI string formats with Zod v4:
|
|
464
|
+
|
|
465
|
+
| OpenAPI Format | Zod v4 Function |
|
|
466
|
+
| -------------- | ------------------ |
|
|
467
|
+
| `uuid` | `z.uuid()` |
|
|
468
|
+
| `email` | `z.email()` |
|
|
469
|
+
| `url`, `uri` | `z.url()` |
|
|
470
|
+
| `date` | `z.iso.date()` |
|
|
471
|
+
| `date-time` | `z.iso.datetime()` |
|
|
472
|
+
| `time` | `z.iso.time()` |
|
|
473
|
+
| `duration` | `z.iso.duration()` |
|
|
474
|
+
| `ipv4` | `z.ipv4()` |
|
|
475
|
+
| `ipv6` | `z.ipv6()` |
|
|
476
|
+
| `emoji` | `z.emoji()` |
|
|
477
|
+
| `base64` | `z.base64()` |
|
|
478
|
+
| `base64url` | `z.base64url()` |
|
|
479
|
+
| `nanoid` | `z.nanoid()` |
|
|
480
|
+
| `cuid` | `z.cuid()` |
|
|
481
|
+
| `cuid2` | `z.cuid2()` |
|
|
482
|
+
| `ulid` | `z.ulid()` |
|
|
483
|
+
| `cidrv4` | `z.cidrv4()` |
|
|
484
|
+
| `cidrv6` | `z.cidrv6()` |
|
|
485
|
+
|
|
486
|
+
### Custom Date-Time Format
|
|
487
|
+
|
|
488
|
+
By default, the generator uses `z.iso.datetime()` for `date-time` format fields, which requires an ISO 8601 datetime string with a timezone suffix (e.g., `2026-01-07T14:30:00Z`).
|
|
489
|
+
|
|
490
|
+
If your API returns date-times **without the `Z` suffix** (e.g., `2026-01-07T14:30:00`), you can override this with a custom regex pattern:
|
|
491
|
+
|
|
492
|
+
```typescript
|
|
493
|
+
import { defineConfig } from "@spartaficial/openapi-to-zod";
|
|
494
|
+
|
|
495
|
+
export default defineConfig({
|
|
496
|
+
defaults: {
|
|
497
|
+
// For date-times without Z suffix
|
|
498
|
+
customDateTimeFormatRegex: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
|
|
499
|
+
},
|
|
500
|
+
specs: [
|
|
501
|
+
{
|
|
502
|
+
input: "openapi.yaml",
|
|
503
|
+
outputTypes: "src/schemas.ts",
|
|
504
|
+
},
|
|
505
|
+
],
|
|
506
|
+
});
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
**TypeScript Config - RegExp Literals:**
|
|
510
|
+
|
|
511
|
+
In TypeScript config files, you can also use RegExp literals (which don't require double-escaping):
|
|
512
|
+
|
|
513
|
+
```typescript
|
|
514
|
+
export default defineConfig({
|
|
515
|
+
defaults: {
|
|
516
|
+
// Use RegExp literal (single escaping)
|
|
517
|
+
customDateTimeFormatRegex: /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/,
|
|
518
|
+
},
|
|
519
|
+
specs: [
|
|
520
|
+
{
|
|
521
|
+
input: "openapi.yaml",
|
|
522
|
+
outputTypes: "src/schemas.ts",
|
|
523
|
+
},
|
|
524
|
+
],
|
|
525
|
+
});
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
**Common Custom Formats:**
|
|
529
|
+
|
|
530
|
+
| Use Case | String Pattern (JSON/YAML) | RegExp Literal (TypeScript) |
|
|
531
|
+
| ----------------------------------------------------------------------- | ---------------------------------------------------------- | -------------------------------------------------- |
|
|
532
|
+
| No timezone suffix<br>`2026-01-07T14:30:00` | `'^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$'` | `/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/` |
|
|
533
|
+
| With milliseconds, no Z<br>`2026-01-07T14:30:00.123` | `'^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}$'` | `/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}$/` |
|
|
534
|
+
| Optional Z suffix<br>`2026-01-07T14:30:00` or<br>`2026-01-07T14:30:00Z` | `'^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z?$'` | `/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z?$/` |
|
|
535
|
+
| With milliseconds + optional Z<br>`2026-01-07T14:30:00.123Z` | `'^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z?$'` | `/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z?$/` |
|
|
536
|
+
|
|
537
|
+
**Generated Output:**
|
|
538
|
+
|
|
539
|
+
When using a custom regex, the generator will produce:
|
|
540
|
+
|
|
541
|
+
```typescript
|
|
542
|
+
// Instead of: z.iso.datetime()
|
|
543
|
+
// You get: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/)
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
**Note:** This option only affects `date-time` format fields. Other formats (like `date`, `email`, `uuid`) remain unchanged.
|
|
547
|
+
|
|
548
|
+
### UUID Format
|
|
549
|
+
|
|
550
|
+
By default, the generator uses `z.uuid()` for `uuid` and `guid` format fields. You can change this with the `uuidFormat` option to use a specific UUID version or GUID validation:
|
|
551
|
+
|
|
552
|
+
```typescript
|
|
553
|
+
import { defineConfig } from "@spartaficial/openapi-to-zod";
|
|
554
|
+
|
|
555
|
+
export default defineConfig({
|
|
556
|
+
defaults: {
|
|
557
|
+
uuidFormat: "uuidv4",
|
|
558
|
+
},
|
|
559
|
+
specs: [
|
|
560
|
+
{
|
|
561
|
+
input: "openapi.yaml",
|
|
562
|
+
outputTypes: "src/schemas.ts",
|
|
563
|
+
},
|
|
564
|
+
],
|
|
565
|
+
});
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
**Available values:**
|
|
569
|
+
|
|
570
|
+
| Value | Generated Output |
|
|
571
|
+
| ----------------------- | -------------------------------- |
|
|
572
|
+
| `"uuid"` (default) | `z.uuid()` |
|
|
573
|
+
| `"guid"` | `z.guid()` |
|
|
574
|
+
| `"uuidv1"` – `"uuidv8"` | `z.uuid({ version: "v1" })` etc. |
|
|
575
|
+
|
|
576
|
+
**Note:** Both `format: "uuid"` and `format: "guid"` in OpenAPI specs follow the configured `uuidFormat` setting.
|
|
577
|
+
|
|
578
|
+
## Advanced Features
|
|
579
|
+
|
|
580
|
+
### Operation Filtering
|
|
581
|
+
|
|
582
|
+
Filter which operations are included in schema generation. This is useful when you want to generate schemas for only a subset of your API.
|
|
583
|
+
|
|
584
|
+
**Example 1: Filter by tags**
|
|
585
|
+
|
|
586
|
+
```typescript
|
|
587
|
+
export default defineConfig({
|
|
588
|
+
specs: [
|
|
589
|
+
{
|
|
590
|
+
input: "openapi.yaml",
|
|
591
|
+
outputTypes: "public-schemas.ts",
|
|
592
|
+
operationFilters: {
|
|
593
|
+
includeTags: ["public", "users"], // Only include operations tagged with 'public' or 'users'
|
|
594
|
+
},
|
|
595
|
+
},
|
|
596
|
+
],
|
|
597
|
+
});
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
**Example 2: Filter by paths**
|
|
601
|
+
|
|
602
|
+
```typescript
|
|
603
|
+
export default defineConfig({
|
|
604
|
+
specs: [
|
|
605
|
+
{
|
|
606
|
+
input: "openapi.yaml",
|
|
607
|
+
outputTypes: "v1-schemas.ts",
|
|
608
|
+
operationFilters: {
|
|
609
|
+
includePaths: ["/api/v1/**"], // Only v1 endpoints
|
|
610
|
+
excludePaths: ["/api/v1/admin/**"], // But exclude admin endpoints
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
],
|
|
614
|
+
});
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
**Example 3: Exclude deprecated operations**
|
|
618
|
+
|
|
619
|
+
```typescript
|
|
620
|
+
export default defineConfig({
|
|
621
|
+
specs: [
|
|
622
|
+
{
|
|
623
|
+
input: "openapi.yaml",
|
|
624
|
+
outputTypes: "current-schemas.ts",
|
|
625
|
+
operationFilters: {
|
|
626
|
+
excludeDeprecated: true, // Skip all deprecated operations
|
|
627
|
+
},
|
|
628
|
+
},
|
|
629
|
+
],
|
|
630
|
+
});
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
**Filtering Logic:**
|
|
634
|
+
|
|
635
|
+
1. If no filters specified, all operations are included
|
|
636
|
+
2. Empty arrays are treated as "no constraint"
|
|
637
|
+
3. Include filters are applied first (allowlist)
|
|
638
|
+
4. Exclude filters are applied second (blocklist)
|
|
639
|
+
5. Exclude rules always win over include rules
|
|
640
|
+
|
|
641
|
+
**Statistics:** When using operation filters, generation statistics will show how many operations were filtered out.
|
|
642
|
+
|
|
643
|
+
### Request/Response Schema Separation
|
|
644
|
+
|
|
645
|
+
Generate separate schemas for requests and responses by filtering `readOnly` and `writeOnly` properties.
|
|
646
|
+
|
|
647
|
+
**Example: Request schemas (exclude readOnly)**
|
|
648
|
+
|
|
649
|
+
```typescript
|
|
650
|
+
export default defineConfig({
|
|
651
|
+
specs: [
|
|
652
|
+
{
|
|
653
|
+
input: "openapi.yaml",
|
|
654
|
+
outputTypes: "request-schemas.ts",
|
|
655
|
+
schemaType: "request", // Excludes readOnly properties like 'id', 'createdAt'
|
|
656
|
+
},
|
|
657
|
+
],
|
|
658
|
+
});
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
**Example: Response schemas (exclude writeOnly)**
|
|
662
|
+
|
|
663
|
+
```typescript
|
|
664
|
+
export default defineConfig({
|
|
665
|
+
specs: [
|
|
666
|
+
{
|
|
667
|
+
input: "openapi.yaml",
|
|
668
|
+
outputTypes: "response-schemas.ts",
|
|
669
|
+
schemaType: "response", // Excludes writeOnly properties like 'password'
|
|
670
|
+
},
|
|
671
|
+
],
|
|
672
|
+
});
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
**Example: Context-specific validation**
|
|
676
|
+
|
|
677
|
+
```typescript
|
|
678
|
+
export default defineConfig({
|
|
679
|
+
specs: [
|
|
680
|
+
{
|
|
681
|
+
input: "openapi.yaml",
|
|
682
|
+
outputTypes: "schemas.ts",
|
|
683
|
+
request: {
|
|
684
|
+
mode: "strict", // Strict validation for incoming data
|
|
685
|
+
includeDescriptions: false,
|
|
686
|
+
},
|
|
687
|
+
response: {
|
|
688
|
+
mode: "loose", // Flexible validation for API responses
|
|
689
|
+
includeDescriptions: true,
|
|
690
|
+
},
|
|
691
|
+
},
|
|
692
|
+
],
|
|
693
|
+
});
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
**OpenAPI Spec:**
|
|
697
|
+
|
|
698
|
+
```yaml
|
|
699
|
+
User:
|
|
700
|
+
type: object
|
|
701
|
+
properties:
|
|
702
|
+
id:
|
|
703
|
+
type: string
|
|
704
|
+
readOnly: true # Excluded in 'request' mode
|
|
705
|
+
email:
|
|
706
|
+
type: string
|
|
707
|
+
password:
|
|
708
|
+
type: string
|
|
709
|
+
writeOnly: true # Excluded in 'response' mode
|
|
710
|
+
createdAt:
|
|
711
|
+
type: string
|
|
712
|
+
format: date-time
|
|
713
|
+
readOnly: true # Excluded in 'request' mode
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
**Generated Request Schema** (`schemaType: 'request'`):
|
|
717
|
+
|
|
718
|
+
```typescript
|
|
719
|
+
export const userSchema = z.strictObject({
|
|
720
|
+
email: z.string(),
|
|
721
|
+
password: z.string(), // writeOnly included
|
|
722
|
+
// id and createdAt excluded (readOnly)
|
|
723
|
+
});
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
**Generated Response Schema** (`schemaType: 'response'`):
|
|
727
|
+
|
|
728
|
+
```typescript
|
|
729
|
+
export const userSchema = z.strictObject({
|
|
730
|
+
id: z.string(), // readOnly included
|
|
731
|
+
email: z.string(),
|
|
732
|
+
createdAt: z.string().datetime(), // readOnly included
|
|
733
|
+
// password excluded (writeOnly)
|
|
734
|
+
});
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
### String Constraints
|
|
738
|
+
|
|
739
|
+
- `minLength` and `maxLength` are automatically applied
|
|
740
|
+
- `pattern` is converted to `.regex()`
|
|
741
|
+
- Formats with constraints are properly chained
|
|
742
|
+
|
|
743
|
+
### Number Constraints
|
|
744
|
+
|
|
745
|
+
- `minimum` becomes `.gte()`
|
|
746
|
+
- `maximum` becomes `.lte()`
|
|
747
|
+
- `integer` type uses `.int()`
|
|
748
|
+
|
|
749
|
+
### Nullable Types
|
|
750
|
+
|
|
751
|
+
OpenAPI's `nullable: true` is converted to `.nullable()`
|
|
752
|
+
|
|
753
|
+
#### Default Nullable Behavior
|
|
754
|
+
|
|
755
|
+
By default, properties are only nullable when explicitly marked with `nullable: true` (OpenAPI 3.0) or `type: ["string", "null"]` (OpenAPI 3.1).
|
|
756
|
+
|
|
757
|
+
However, many teams follow the industry de facto standard for OpenAPI 3.0.x where properties are assumed nullable unless explicitly constrained. You can enable this behavior with the `defaultNullable` option:
|
|
758
|
+
|
|
759
|
+
```typescript
|
|
760
|
+
export default defineConfig({
|
|
761
|
+
specs: [
|
|
762
|
+
{
|
|
763
|
+
input: "openapi.yaml",
|
|
764
|
+
outputTypes: "schemas.ts",
|
|
765
|
+
defaultNullable: true, // Treat unspecified properties as nullable
|
|
766
|
+
},
|
|
767
|
+
],
|
|
768
|
+
});
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
**Important:** `defaultNullable` only applies to **primitive property values** within objects. It does NOT apply to:
|
|
772
|
+
|
|
773
|
+
- **Top-level schema definitions** - Schemas are not made nullable at the definition level
|
|
774
|
+
- **Schema references (`$ref`)** - References preserve the nullability of the target schema; add explicit `nullable: true` if needed
|
|
775
|
+
- **Enum values** - Enums define discrete values and are not nullable by default
|
|
776
|
+
- **Const/literal values** - Literals are exact values and are not nullable by default
|
|
777
|
+
|
|
778
|
+
**Behavior comparison:**
|
|
779
|
+
|
|
780
|
+
| Schema Property | `defaultNullable: false` (default) | `defaultNullable: true` |
|
|
781
|
+
| ------------------------- | ---------------------------------- | ----------------------- |
|
|
782
|
+
| `nullable: true` | `.nullable()` | `.nullable()` |
|
|
783
|
+
| `nullable: false` | No `.nullable()` | No `.nullable()` |
|
|
784
|
+
| No annotation (primitive) | No `.nullable()` | `.nullable()` |
|
|
785
|
+
| No annotation (`$ref`) | No `.nullable()` | No `.nullable()` |
|
|
786
|
+
| No annotation (enum) | No `.nullable()` | No `.nullable()` |
|
|
787
|
+
| No annotation (const) | No `.nullable()` | No `.nullable()` |
|
|
788
|
+
|
|
789
|
+
**Example:**
|
|
790
|
+
|
|
791
|
+
```yaml
|
|
792
|
+
components:
|
|
793
|
+
schemas:
|
|
794
|
+
Status:
|
|
795
|
+
type: string
|
|
796
|
+
enum: [active, inactive]
|
|
797
|
+
User:
|
|
798
|
+
type: object
|
|
799
|
+
properties:
|
|
800
|
+
id:
|
|
801
|
+
type: integer
|
|
802
|
+
name:
|
|
803
|
+
type: string
|
|
804
|
+
status:
|
|
805
|
+
$ref: "#/components/schemas/Status"
|
|
806
|
+
nullableStatus:
|
|
807
|
+
allOf:
|
|
808
|
+
- $ref: "#/components/schemas/Status"
|
|
809
|
+
nullable: true
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
**With `defaultNullable: false` (default):**
|
|
813
|
+
|
|
814
|
+
```typescript
|
|
815
|
+
export const statusSchema = z.enum(["active", "inactive"]);
|
|
816
|
+
|
|
817
|
+
export const userSchema = z.strictObject({
|
|
818
|
+
id: z.number().int(),
|
|
819
|
+
name: z.string(), // Not nullable (no annotation)
|
|
820
|
+
status: statusSchema, // Not nullable ($ref)
|
|
821
|
+
nullableStatus: statusSchema.nullable(), // Explicitly nullable
|
|
822
|
+
});
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
**With `defaultNullable: true`:**
|
|
826
|
+
|
|
827
|
+
```typescript
|
|
828
|
+
export const statusSchema = z.enum(["active", "inactive"]);
|
|
829
|
+
|
|
830
|
+
export const userSchema = z.strictObject({
|
|
831
|
+
id: z.number().int().nullable(), // Nullable (primitive)
|
|
832
|
+
name: z.string().nullable(), // Nullable (primitive)
|
|
833
|
+
status: statusSchema, // NOT nullable ($ref - must be explicit)
|
|
834
|
+
nullableStatus: statusSchema.nullable(), // Explicitly nullable
|
|
835
|
+
});
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
### Schema Composition
|
|
839
|
+
|
|
840
|
+
- `allOf` → `.extend()` for objects (Zod v4), `.and()` for primitives
|
|
841
|
+
- `oneOf`, `anyOf` → `z.union()` or `z.discriminatedUnion()`
|
|
842
|
+
- `$ref` → Proper schema references
|
|
843
|
+
|
|
844
|
+
### Enums
|
|
845
|
+
|
|
846
|
+
Enums are generated based on their value types:
|
|
847
|
+
|
|
848
|
+
- **String enums**: `z.enum()` for type-safe string unions
|
|
849
|
+
- **Numeric enums**: `z.union([z.literal(n), ...])` for proper number types
|
|
850
|
+
- **Boolean enums**: `z.boolean()` for true/false values
|
|
851
|
+
- **Mixed enums**: `z.union([z.literal(...), ...])` for heterogeneous values
|
|
852
|
+
|
|
853
|
+
**Examples:**
|
|
854
|
+
|
|
855
|
+
```yaml
|
|
856
|
+
# String enum
|
|
857
|
+
Status:
|
|
858
|
+
type: string
|
|
859
|
+
enum: [active, inactive, pending]
|
|
860
|
+
|
|
861
|
+
# Integer enum
|
|
862
|
+
Priority:
|
|
863
|
+
type: integer
|
|
864
|
+
enum: [0, 1, 2, 3]
|
|
865
|
+
|
|
866
|
+
# Mixed enum
|
|
867
|
+
Value:
|
|
868
|
+
enum: [0, "none", 1, "some"]
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
**Generated schemas:**
|
|
872
|
+
|
|
873
|
+
```typescript
|
|
874
|
+
// String enum → z.enum()
|
|
875
|
+
export const statusSchema = z.enum(["active", "inactive", "pending"]);
|
|
876
|
+
export type Status = z.infer<typeof statusSchema>; // "active" | "inactive" | "pending"
|
|
877
|
+
|
|
878
|
+
// Integer enum → z.union with z.literal
|
|
879
|
+
export const prioritySchema = z.union([z.literal(0), z.literal(1), z.literal(2), z.literal(3)]);
|
|
880
|
+
export type Priority = z.infer<typeof prioritySchema>; // 0 | 1 | 2 | 3
|
|
881
|
+
|
|
882
|
+
// Mixed enum → z.union with z.literal
|
|
883
|
+
export const valueSchema = z.union([z.literal(0), z.literal("none"), z.literal(1), z.literal("some")]);
|
|
884
|
+
export type Value = z.infer<typeof valueSchema>; // 0 | "none" | 1 | "some"
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
## Schema Naming
|
|
888
|
+
|
|
889
|
+
Customize schema names with prefixes and suffixes:
|
|
890
|
+
|
|
891
|
+
```typescript
|
|
892
|
+
// In your config file
|
|
893
|
+
export default defineConfig({
|
|
894
|
+
specs: [
|
|
895
|
+
{
|
|
896
|
+
input: "openapi.yaml",
|
|
897
|
+
outputTypes: "schemas.ts",
|
|
898
|
+
prefix: "api", // Output: apiUserSchema, apiProductSchema
|
|
899
|
+
suffix: "dto", // Output: userDtoSchema, productDtoSchema
|
|
900
|
+
},
|
|
901
|
+
],
|
|
902
|
+
});
|
|
903
|
+
```
|
|
904
|
+
|
|
905
|
+
This is useful when:
|
|
906
|
+
|
|
907
|
+
- Working with multiple API specs in the same project
|
|
908
|
+
- Following specific naming conventions (DTO, Model, Entity)
|
|
909
|
+
- Avoiding naming conflicts with existing code
|
|
910
|
+
|
|
911
|
+
### Request and response shapes
|
|
912
|
+
|
|
913
|
+
`readOnly` and `writeOnly` mean one schema describes two shapes - what you send and what you
|
|
914
|
+
receive. `schemaType` picks which one to generate.
|
|
915
|
+
|
|
916
|
+
See [Request and response shapes](../../README.md#request-and-response-shapes-schematype) for
|
|
917
|
+
the worked example and which packages accept the option.
|
|
918
|
+
|
|
919
|
+
### Specs Split Across Files
|
|
920
|
+
|
|
921
|
+
A `$ref` may point into another document, including an absolute URL from a shared schema
|
|
922
|
+
registry. Referenced documents are read when the spec is loaded and inlined into your spec, so
|
|
923
|
+
the generated file is self-contained. Remote URLs are never fetched - map them to local files
|
|
924
|
+
with `refMappings`.
|
|
925
|
+
|
|
926
|
+
See [Specs split across files](../../README.md#specs-split-across-files) for how paths resolve,
|
|
927
|
+
how `refMappings` matching works, and what happens on a name clash.
|
|
928
|
+
|
|
929
|
+
### Schema Prefix Stripping
|
|
930
|
+
|
|
931
|
+
The `stripSchemaPrefix` option removes common prefixes from schema names in your OpenAPI spec before generating Zod schemas. This is particularly useful when your OpenAPI spec uses namespaced schema names (like .NET-generated specs with "Company.Models.User").
|
|
932
|
+
|
|
933
|
+
**OpenAPI Spec with Namespaced Schemas:**
|
|
934
|
+
|
|
935
|
+
```yaml
|
|
936
|
+
components:
|
|
937
|
+
schemas:
|
|
938
|
+
Company.Models.User:
|
|
939
|
+
type: object
|
|
940
|
+
properties:
|
|
941
|
+
id:
|
|
942
|
+
type: string
|
|
943
|
+
name:
|
|
944
|
+
type: string
|
|
945
|
+
role:
|
|
946
|
+
$ref: "#/components/schemas/Company.Models.UserRole"
|
|
947
|
+
Company.Models.UserRole:
|
|
948
|
+
type: string
|
|
949
|
+
enum: [admin, user, guest]
|
|
950
|
+
Company.Models.Post:
|
|
951
|
+
type: object
|
|
952
|
+
properties:
|
|
953
|
+
id:
|
|
954
|
+
type: string
|
|
955
|
+
title:
|
|
956
|
+
type: string
|
|
957
|
+
author:
|
|
958
|
+
$ref: "#/components/schemas/Company.Models.User"
|
|
959
|
+
```
|
|
960
|
+
|
|
961
|
+
**Without `stripSchemaPrefix`:**
|
|
962
|
+
|
|
963
|
+
```typescript
|
|
964
|
+
export const companyModelsUserRoleSchema = z.enum(["admin", "user", "guest"]);
|
|
965
|
+
|
|
966
|
+
export const companyModelsUserSchema = z.strictObject({
|
|
967
|
+
id: z.string(),
|
|
968
|
+
name: z.string(),
|
|
969
|
+
role: companyModelsUserRoleSchema, // Long reference name
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
export const companyModelsPostSchema = z.strictObject({
|
|
973
|
+
id: z.string(),
|
|
974
|
+
title: z.string(),
|
|
975
|
+
author: companyModelsUserSchema, // Long reference name
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
export type CompanyModelsUserRole = z.infer<typeof companyModelsUserRoleSchema>;
|
|
979
|
+
export type CompanyModelsUser = z.infer<typeof companyModelsUserSchema>;
|
|
980
|
+
export type CompanyModelsPost = z.infer<typeof companyModelsPostSchema>;
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
**With `stripSchemaPrefix: "Company.Models."`:**
|
|
984
|
+
|
|
985
|
+
```typescript
|
|
986
|
+
export const userRoleSchema = z.enum(["admin", "user", "guest"]);
|
|
987
|
+
|
|
988
|
+
export const userSchema = z.strictObject({
|
|
989
|
+
id: z.string(),
|
|
990
|
+
name: z.string(),
|
|
991
|
+
role: userRoleSchema, // Clean reference
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
export const postSchema = z.strictObject({
|
|
995
|
+
id: z.string(),
|
|
996
|
+
title: z.string(),
|
|
997
|
+
author: userSchema, // Clean reference
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
export type UserRole = z.infer<typeof userRoleSchema>;
|
|
1001
|
+
export type User = z.infer<typeof userSchema>;
|
|
1002
|
+
export type Post = z.infer<typeof postSchema>;
|
|
1003
|
+
```
|
|
1004
|
+
|
|
1005
|
+
#### Usage
|
|
1006
|
+
|
|
1007
|
+
```typescript
|
|
1008
|
+
export default defineConfig({
|
|
1009
|
+
specs: [
|
|
1010
|
+
{
|
|
1011
|
+
input: "openapi.yaml",
|
|
1012
|
+
outputTypes: "schemas.ts",
|
|
1013
|
+
stripSchemaPrefix: "Company.Models.", // Strip this exact prefix
|
|
1014
|
+
},
|
|
1015
|
+
],
|
|
1016
|
+
});
|
|
1017
|
+
```
|
|
1018
|
+
|
|
1019
|
+
#### Glob Patterns
|
|
1020
|
+
|
|
1021
|
+
Use glob patterns to strip dynamic prefixes:
|
|
1022
|
+
|
|
1023
|
+
```typescript
|
|
1024
|
+
export default defineConfig({
|
|
1025
|
+
specs: [
|
|
1026
|
+
{
|
|
1027
|
+
input: "openapi.yaml",
|
|
1028
|
+
outputTypes: "schemas.ts",
|
|
1029
|
+
// Strip any namespace prefix with wildcard
|
|
1030
|
+
stripSchemaPrefix: "*.Models.",
|
|
1031
|
+
},
|
|
1032
|
+
],
|
|
1033
|
+
});
|
|
1034
|
+
```
|
|
1035
|
+
|
|
1036
|
+
**Glob Pattern Syntax:**
|
|
1037
|
+
|
|
1038
|
+
Glob patterns support powerful matching using [minimatch](https://github.com/isaacs/minimatch):
|
|
1039
|
+
|
|
1040
|
+
- `*` matches any characters within a single segment (stops at `.`)
|
|
1041
|
+
- `**` matches any characters across multiple segments (crosses `.` boundaries)
|
|
1042
|
+
- `?` matches a single character
|
|
1043
|
+
- `[abc]` matches any character in the set
|
|
1044
|
+
- `{a,b}` matches any of the alternatives
|
|
1045
|
+
- `!(pattern)` matches anything except the pattern
|
|
1046
|
+
|
|
1047
|
+
```typescript
|
|
1048
|
+
// Examples of glob patterns:
|
|
1049
|
+
stripSchemaPrefix: "*.Models."; // Matches Company.Models., App.Models.
|
|
1050
|
+
stripSchemaPrefix: "**.Models."; // Matches any depth: Company.Api.Models., App.V2.Models.
|
|
1051
|
+
stripSchemaPrefix: "Company.{Models,Services}."; // Matches Company.Models. or Company.Services.
|
|
1052
|
+
stripSchemaPrefix: "api_v[0-9]_"; // Matches api_v1_, api_v2_, etc.
|
|
1053
|
+
stripSchemaPrefix: "v*.*."; // Matches v1.0., v2.1., etc.
|
|
1054
|
+
stripSchemaPrefix: "!(Internal)*."; // Matches any prefix except those starting with Internal
|
|
1055
|
+
```
|
|
1056
|
+
|
|
1057
|
+
#### Common Patterns
|
|
1058
|
+
|
|
1059
|
+
**Pattern 1: .NET Namespaces**
|
|
1060
|
+
|
|
1061
|
+
```typescript
|
|
1062
|
+
{
|
|
1063
|
+
stripSchemaPrefix: "Company.Models.";
|
|
1064
|
+
}
|
|
1065
|
+
// Company.Models.User → User
|
|
1066
|
+
// Company.Models.Post → Post
|
|
1067
|
+
```
|
|
1068
|
+
|
|
1069
|
+
**Pattern 2: Multiple Namespaces with Wildcard**
|
|
1070
|
+
|
|
1071
|
+
```typescript
|
|
1072
|
+
{
|
|
1073
|
+
stripSchemaPrefix: "*.Models.";
|
|
1074
|
+
}
|
|
1075
|
+
// MyApp.Models.User → User
|
|
1076
|
+
// OtherApp.Models.User → User
|
|
1077
|
+
// Company.Models.Post → Post
|
|
1078
|
+
```
|
|
1079
|
+
|
|
1080
|
+
**Pattern 3: Multiple Namespace Types**
|
|
1081
|
+
|
|
1082
|
+
```typescript
|
|
1083
|
+
{
|
|
1084
|
+
stripSchemaPrefix: "*.{Models,Services}.";
|
|
1085
|
+
}
|
|
1086
|
+
// App.Models.User → User
|
|
1087
|
+
// App.Services.UserService → UserService
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
**Pattern 4: Version Prefixes with Character Class**
|
|
1091
|
+
|
|
1092
|
+
```typescript
|
|
1093
|
+
{
|
|
1094
|
+
stripSchemaPrefix: "v[0-9].";
|
|
1095
|
+
}
|
|
1096
|
+
// v1.User → User
|
|
1097
|
+
// v2.Product → Product
|
|
1098
|
+
```
|
|
1099
|
+
|
|
1100
|
+
**Pattern 5: Versioned Prefixes with Wildcards**
|
|
1101
|
+
|
|
1102
|
+
```typescript
|
|
1103
|
+
{
|
|
1104
|
+
stripSchemaPrefix: "api_v*_";
|
|
1105
|
+
}
|
|
1106
|
+
// api_v1_User → User
|
|
1107
|
+
// api_v2_Product → Product
|
|
1108
|
+
// api_v10_Comment → Comment
|
|
1109
|
+
```
|
|
1110
|
+
|
|
1111
|
+
#### Interaction with prefix/suffix Options
|
|
1112
|
+
|
|
1113
|
+
`stripSchemaPrefix` is applied **before** `prefix` and `suffix` options:
|
|
1114
|
+
|
|
1115
|
+
```typescript
|
|
1116
|
+
export default defineConfig({
|
|
1117
|
+
specs: [
|
|
1118
|
+
{
|
|
1119
|
+
input: "openapi.yaml",
|
|
1120
|
+
outputTypes: "schemas.ts",
|
|
1121
|
+
stripSchemaPrefix: "Company.Models.", // Applied first
|
|
1122
|
+
prefix: "api", // Applied second
|
|
1123
|
+
suffix: "dto", // Applied third
|
|
1124
|
+
},
|
|
1125
|
+
],
|
|
1126
|
+
});
|
|
1127
|
+
```
|
|
1128
|
+
|
|
1129
|
+
**Result:**
|
|
1130
|
+
|
|
1131
|
+
- `Company.Models.User` → `User` → `apiUserDtoSchema`
|
|
1132
|
+
- `Company.Models.Post` → `Post` → `apiPostDtoSchema`
|
|
1133
|
+
|
|
1134
|
+
#### Benefits
|
|
1135
|
+
|
|
1136
|
+
1. **Cleaner Schema Names**: Generates `userSchema` instead of `companyModelsUserSchema`
|
|
1137
|
+
2. **Better Type Names**: Creates `User` type instead of `CompanyModelsUser`
|
|
1138
|
+
3. **Shorter References**: Simpler schema references in composed types
|
|
1139
|
+
4. **Better Code Completion**: Easier to find schemas in IDE autocomplete
|
|
1140
|
+
5. **Flexible Pattern Matching**: Use regex for dynamic prefixes
|
|
1141
|
+
|
|
1142
|
+
## Circular References and `z.lazy()`
|
|
1143
|
+
|
|
1144
|
+
When your OpenAPI spec contains circular references (schemas that reference themselves or each other), Zod requires using `z.lazy()` for recursive types. However, this creates a TypeScript challenge:
|
|
1145
|
+
|
|
1146
|
+
```typescript
|
|
1147
|
+
// Combined mode - can cause TypeScript errors
|
|
1148
|
+
export const nodeSchema = z.strictObject({
|
|
1149
|
+
id: z.string(),
|
|
1150
|
+
parent: z.lazy(() => nodeSchema).optional(), // ❌ Type errors with circular inference
|
|
1151
|
+
});
|
|
1152
|
+
export type Node = z.infer<typeof nodeSchema>; // Circular type reference
|
|
1153
|
+
```
|
|
1154
|
+
|
|
1155
|
+
**Recommendation:** Use separate type and schema files (`outputZodSchemas`) for specs with circular references:
|
|
1156
|
+
|
|
1157
|
+
```typescript
|
|
1158
|
+
import { defineConfig } from "@spartaficial/openapi-to-zod";
|
|
1159
|
+
|
|
1160
|
+
export default defineConfig({
|
|
1161
|
+
specs: [
|
|
1162
|
+
{
|
|
1163
|
+
input: "openapi.yaml",
|
|
1164
|
+
outputTypes: "src/generated/types.ts", // TypeScript types
|
|
1165
|
+
outputZodSchemas: "src/generated/schemas.ts", // Zod schemas
|
|
1166
|
+
},
|
|
1167
|
+
],
|
|
1168
|
+
});
|
|
1169
|
+
```
|
|
1170
|
+
|
|
1171
|
+
This generates proper forward-declared types:
|
|
1172
|
+
|
|
1173
|
+
```typescript
|
|
1174
|
+
// types.ts
|
|
1175
|
+
export interface Node {
|
|
1176
|
+
id?: string;
|
|
1177
|
+
parent?: Node;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// schemas.ts
|
|
1181
|
+
import type { Node } from "./types";
|
|
1182
|
+
export const nodeSchema: z.ZodType<Node> = z.strictObject({
|
|
1183
|
+
id: z.string().optional(),
|
|
1184
|
+
parent: z.lazy(() => nodeSchema).optional(),
|
|
1185
|
+
});
|
|
1186
|
+
```
|
|
1187
|
+
|
|
1188
|
+
This approach also helps avoid "Type instantiation is excessively deep" errors (TS2589) with large schemas.
|
|
1189
|
+
|
|
1190
|
+
## Generation Statistics
|
|
1191
|
+
|
|
1192
|
+
Statistics are **included by default** in generated files. Use `showStats: false` to disable:
|
|
1193
|
+
|
|
1194
|
+
```typescript
|
|
1195
|
+
// Generation Statistics:
|
|
1196
|
+
// Total schemas: 42
|
|
1197
|
+
// Circular references: 3
|
|
1198
|
+
// Discriminated unions: 5
|
|
1199
|
+
// With constraints: 18
|
|
1200
|
+
// Generated at: 2025-12-07T06:21:47.634Z
|
|
1201
|
+
```
|
|
1202
|
+
|
|
1203
|
+
Helpful for:
|
|
1204
|
+
|
|
1205
|
+
- Understanding your API complexity
|
|
1206
|
+
- Tracking changes over time
|
|
1207
|
+
- Debugging generation issues
|
|
1208
|
+
|
|
1209
|
+
## OpenAPI Features Supported
|
|
1210
|
+
|
|
1211
|
+
### Basic Types
|
|
1212
|
+
|
|
1213
|
+
#### String Constraints
|
|
1214
|
+
|
|
1215
|
+
- `minLength` → `.min(n)`
|
|
1216
|
+
- `maxLength` → `.max(n)`
|
|
1217
|
+
- `pattern` → `.regex(/pattern/)`
|
|
1218
|
+
- `format` → Specific Zod validators (see Format Support section)
|
|
1219
|
+
|
|
1220
|
+
#### Number Constraints
|
|
1221
|
+
|
|
1222
|
+
- `minimum` → `.gte(n)` (inclusive)
|
|
1223
|
+
- `maximum` → `.lte(n)` (inclusive)
|
|
1224
|
+
- `exclusiveMinimum` → `.gt(n)` (OpenAPI 3.0 boolean or 3.1 number)
|
|
1225
|
+
- `exclusiveMaximum` → `.lt(n)` (OpenAPI 3.0 boolean or 3.1 number)
|
|
1226
|
+
- `multipleOf` → `.multipleOf(n)`
|
|
1227
|
+
- `integer` type → `.int()`
|
|
1228
|
+
|
|
1229
|
+
**Example:**
|
|
1230
|
+
|
|
1231
|
+
```yaml
|
|
1232
|
+
Price:
|
|
1233
|
+
type: number
|
|
1234
|
+
minimum: 0
|
|
1235
|
+
maximum: 10000
|
|
1236
|
+
multipleOf: 0.01 # Enforces 2 decimal places
|
|
1237
|
+
```
|
|
1238
|
+
|
|
1239
|
+
**Generated:**
|
|
1240
|
+
|
|
1241
|
+
```typescript
|
|
1242
|
+
export const priceSchema = z.number().gte(0).lte(10000).multipleOf(0.01);
|
|
1243
|
+
```
|
|
1244
|
+
|
|
1245
|
+
#### Exclusive Bounds (OpenAPI 3.0 & 3.1)
|
|
1246
|
+
|
|
1247
|
+
**OpenAPI 3.0 Style (boolean):**
|
|
1248
|
+
|
|
1249
|
+
```yaml
|
|
1250
|
+
Percentage:
|
|
1251
|
+
type: number
|
|
1252
|
+
minimum: 0
|
|
1253
|
+
maximum: 100
|
|
1254
|
+
exclusiveMinimum: true
|
|
1255
|
+
exclusiveMaximum: true
|
|
1256
|
+
```
|
|
1257
|
+
|
|
1258
|
+
**OpenAPI 3.1 Style (number):**
|
|
1259
|
+
|
|
1260
|
+
```yaml
|
|
1261
|
+
Score:
|
|
1262
|
+
type: number
|
|
1263
|
+
exclusiveMinimum: 0
|
|
1264
|
+
exclusiveMaximum: 100
|
|
1265
|
+
```
|
|
1266
|
+
|
|
1267
|
+
**Both generate:**
|
|
1268
|
+
|
|
1269
|
+
```typescript
|
|
1270
|
+
export const percentageSchema = z.number().gt(0).lt(100);
|
|
1271
|
+
```
|
|
1272
|
+
|
|
1273
|
+
### Array Features
|
|
1274
|
+
|
|
1275
|
+
#### Basic Array Constraints
|
|
1276
|
+
|
|
1277
|
+
- `minItems` → `.min(n)`
|
|
1278
|
+
- `maxItems` → `.max(n)`
|
|
1279
|
+
- `uniqueItems: true` → `.refine()` with Set-based validation
|
|
1280
|
+
|
|
1281
|
+
**Example:**
|
|
1282
|
+
|
|
1283
|
+
```yaml
|
|
1284
|
+
UniqueTags:
|
|
1285
|
+
type: array
|
|
1286
|
+
items:
|
|
1287
|
+
type: string
|
|
1288
|
+
uniqueItems: true
|
|
1289
|
+
minItems: 1
|
|
1290
|
+
maxItems: 10
|
|
1291
|
+
```
|
|
1292
|
+
|
|
1293
|
+
**Generated:**
|
|
1294
|
+
|
|
1295
|
+
```typescript
|
|
1296
|
+
export const uniqueTagsSchema = z
|
|
1297
|
+
.array(z.string())
|
|
1298
|
+
.min(1)
|
|
1299
|
+
.max(10)
|
|
1300
|
+
.refine(items => new Set(items).size === items.length, {
|
|
1301
|
+
message: "Array items must be unique",
|
|
1302
|
+
});
|
|
1303
|
+
```
|
|
1304
|
+
|
|
1305
|
+
#### Tuple Validation (OpenAPI 3.1)
|
|
1306
|
+
|
|
1307
|
+
**OpenAPI Spec Support**: 3.1+
|
|
1308
|
+
|
|
1309
|
+
Use `prefixItems` for fixed-position array types (tuples):
|
|
1310
|
+
|
|
1311
|
+
```yaml
|
|
1312
|
+
Coordinates:
|
|
1313
|
+
type: array
|
|
1314
|
+
description: Geographic coordinates as [latitude, longitude]
|
|
1315
|
+
prefixItems:
|
|
1316
|
+
- type: number
|
|
1317
|
+
minimum: -90
|
|
1318
|
+
maximum: 90
|
|
1319
|
+
- type: number
|
|
1320
|
+
minimum: -180
|
|
1321
|
+
maximum: 180
|
|
1322
|
+
minItems: 2
|
|
1323
|
+
maxItems: 2
|
|
1324
|
+
```
|
|
1325
|
+
|
|
1326
|
+
**Generated:**
|
|
1327
|
+
|
|
1328
|
+
```typescript
|
|
1329
|
+
export const coordinatesSchema = z.tuple([z.number().gte(-90).lte(90), z.number().gte(-180).lte(180)]);
|
|
1330
|
+
```
|
|
1331
|
+
|
|
1332
|
+
**With Rest Items:**
|
|
1333
|
+
|
|
1334
|
+
```yaml
|
|
1335
|
+
CommandArgs:
|
|
1336
|
+
type: array
|
|
1337
|
+
prefixItems:
|
|
1338
|
+
- type: string # Command name
|
|
1339
|
+
- type: string # Action
|
|
1340
|
+
items:
|
|
1341
|
+
type: string # Additional arguments
|
|
1342
|
+
```
|
|
1343
|
+
|
|
1344
|
+
**Generated:**
|
|
1345
|
+
|
|
1346
|
+
```typescript
|
|
1347
|
+
export const commandArgsSchema = z.tuple([z.string(), z.string()]).rest(z.string());
|
|
1348
|
+
```
|
|
1349
|
+
|
|
1350
|
+
### Object Features
|
|
1351
|
+
|
|
1352
|
+
#### Property Constraints
|
|
1353
|
+
|
|
1354
|
+
- `required` array → Properties without `.optional()`
|
|
1355
|
+
- `additionalProperties: false` → `.strict()` (or implicit in strict mode)
|
|
1356
|
+
- `additionalProperties: true` → `.catchall(z.unknown())`
|
|
1357
|
+
- `additionalProperties: {schema}` → `.catchall(schema)`
|
|
1358
|
+
- `minProperties` → `.refine()` with property count validation
|
|
1359
|
+
- `maxProperties` → `.refine()` with property count validation
|
|
1360
|
+
|
|
1361
|
+
**Example:**
|
|
1362
|
+
|
|
1363
|
+
```yaml
|
|
1364
|
+
FlexibleMetadata:
|
|
1365
|
+
type: object
|
|
1366
|
+
minProperties: 1
|
|
1367
|
+
maxProperties: 10
|
|
1368
|
+
additionalProperties:
|
|
1369
|
+
type: string
|
|
1370
|
+
```
|
|
1371
|
+
|
|
1372
|
+
**Generated:**
|
|
1373
|
+
|
|
1374
|
+
```typescript
|
|
1375
|
+
export const flexibleMetadataSchema = z
|
|
1376
|
+
.object({})
|
|
1377
|
+
.catchall(z.string())
|
|
1378
|
+
.refine(obj => Object.keys(obj).length >= 1 && Object.keys(obj).length <= 10, {
|
|
1379
|
+
message: "Object must have between 1 and 10 properties",
|
|
1380
|
+
});
|
|
1381
|
+
```
|
|
1382
|
+
|
|
1383
|
+
### Schema Composition
|
|
1384
|
+
|
|
1385
|
+
#### AllOf - Smart Extending
|
|
1386
|
+
|
|
1387
|
+
Uses `.extend()` for objects (Zod v4 compliant - `.merge()` is deprecated), `.and()` for primitives:
|
|
1388
|
+
|
|
1389
|
+
**Object Extending:**
|
|
1390
|
+
|
|
1391
|
+
```yaml
|
|
1392
|
+
User:
|
|
1393
|
+
allOf:
|
|
1394
|
+
- $ref: "#/components/schemas/BaseEntity"
|
|
1395
|
+
- $ref: "#/components/schemas/Timestamped"
|
|
1396
|
+
- type: object
|
|
1397
|
+
properties:
|
|
1398
|
+
username:
|
|
1399
|
+
type: string
|
|
1400
|
+
required:
|
|
1401
|
+
- username
|
|
1402
|
+
```
|
|
1403
|
+
|
|
1404
|
+
**Generated:**
|
|
1405
|
+
|
|
1406
|
+
```typescript
|
|
1407
|
+
export const userSchema = baseEntitySchema.extend(timestampedSchema.shape).extend(
|
|
1408
|
+
z.object({
|
|
1409
|
+
username: z.string(),
|
|
1410
|
+
}).shape
|
|
1411
|
+
);
|
|
1412
|
+
```
|
|
1413
|
+
|
|
1414
|
+
#### OneOf / AnyOf
|
|
1415
|
+
|
|
1416
|
+
- `oneOf` → `z.union()` or `z.discriminatedUnion()` (if discriminator present)
|
|
1417
|
+
- `anyOf` → `z.union()` or `z.discriminatedUnion()` (if discriminator present)
|
|
1418
|
+
|
|
1419
|
+
### Nullable Types
|
|
1420
|
+
|
|
1421
|
+
**OpenAPI 3.0 Style:**
|
|
1422
|
+
|
|
1423
|
+
```yaml
|
|
1424
|
+
NullableString:
|
|
1425
|
+
type: string
|
|
1426
|
+
nullable: true
|
|
1427
|
+
```
|
|
1428
|
+
|
|
1429
|
+
**OpenAPI 3.1 Style:**
|
|
1430
|
+
|
|
1431
|
+
```yaml
|
|
1432
|
+
NullableString:
|
|
1433
|
+
type: ["string", "null"]
|
|
1434
|
+
```
|
|
1435
|
+
|
|
1436
|
+
**Both generate:**
|
|
1437
|
+
|
|
1438
|
+
```typescript
|
|
1439
|
+
export const nullableStringSchema = z.string().nullable();
|
|
1440
|
+
```
|
|
1441
|
+
|
|
1442
|
+
### Literal Values
|
|
1443
|
+
|
|
1444
|
+
Use `const` for exact value matching:
|
|
1445
|
+
|
|
1446
|
+
```yaml
|
|
1447
|
+
Environment:
|
|
1448
|
+
type: string
|
|
1449
|
+
const: "production"
|
|
1450
|
+
```
|
|
1451
|
+
|
|
1452
|
+
**Generated:**
|
|
1453
|
+
|
|
1454
|
+
```typescript
|
|
1455
|
+
export const environmentSchema = z.literal("production");
|
|
1456
|
+
```
|
|
1457
|
+
|
|
1458
|
+
### Deprecation Support
|
|
1459
|
+
|
|
1460
|
+
Mark schemas or properties as deprecated:
|
|
1461
|
+
|
|
1462
|
+
```yaml
|
|
1463
|
+
OldUser:
|
|
1464
|
+
type: object
|
|
1465
|
+
deprecated: true
|
|
1466
|
+
description: Legacy user schema
|
|
1467
|
+
properties:
|
|
1468
|
+
legacyId:
|
|
1469
|
+
type: integer
|
|
1470
|
+
deprecated: true
|
|
1471
|
+
description: Old ID format, use uuid instead
|
|
1472
|
+
```
|
|
1473
|
+
|
|
1474
|
+
**Generated:**
|
|
1475
|
+
|
|
1476
|
+
```typescript
|
|
1477
|
+
/** Legacy user schema @deprecated */
|
|
1478
|
+
export const oldUserSchema = z.strictObject({
|
|
1479
|
+
/** Old ID format, use uuid instead @deprecated */
|
|
1480
|
+
legacyId: z.number().int().optional(),
|
|
1481
|
+
});
|
|
1482
|
+
```
|
|
1483
|
+
|
|
1484
|
+
### Metadata & Documentation
|
|
1485
|
+
|
|
1486
|
+
#### Title Field
|
|
1487
|
+
|
|
1488
|
+
```yaml
|
|
1489
|
+
UserAccount:
|
|
1490
|
+
title: User Account
|
|
1491
|
+
description: Represents a user account in the system
|
|
1492
|
+
type: object
|
|
1493
|
+
```
|
|
1494
|
+
|
|
1495
|
+
**Generated:**
|
|
1496
|
+
|
|
1497
|
+
```typescript
|
|
1498
|
+
/** User Account Represents a user account in the system */
|
|
1499
|
+
export const userAccountSchema = z.strictObject({
|
|
1500
|
+
id: z.string(),
|
|
1501
|
+
});
|
|
1502
|
+
```
|
|
1503
|
+
|
|
1504
|
+
#### Examples
|
|
1505
|
+
|
|
1506
|
+
```yaml
|
|
1507
|
+
StatusCode:
|
|
1508
|
+
title: HTTP Status Code
|
|
1509
|
+
type: string
|
|
1510
|
+
enum: ["200", "201", "400", "404", "500"]
|
|
1511
|
+
examples:
|
|
1512
|
+
- "200"
|
|
1513
|
+
- "404"
|
|
1514
|
+
- "500"
|
|
1515
|
+
```
|
|
1516
|
+
|
|
1517
|
+
**Generated:**
|
|
1518
|
+
|
|
1519
|
+
```typescript
|
|
1520
|
+
/** HTTP Status Code @example "200", "404", "500" */
|
|
1521
|
+
export const statusCodeSchema = z.enum(["200", "201", "400", "404", "500"]);
|
|
1522
|
+
```
|
|
1523
|
+
|
|
1524
|
+
### Feature Matrix
|
|
1525
|
+
|
|
1526
|
+
| Feature | OpenAPI 3.0 | OpenAPI 3.1 | Zod Method |
|
|
1527
|
+
| --------------------------- | ----------- | ----------- | -------------------------------- |
|
|
1528
|
+
| Basic types | ✅ | ✅ | `z.string()`, `z.number()`, etc. |
|
|
1529
|
+
| String constraints | ✅ | ✅ | `.min()`, `.max()`, `.regex()` |
|
|
1530
|
+
| Number constraints | ✅ | ✅ | `.gte()`, `.lte()`, `.int()` |
|
|
1531
|
+
| Exclusive bounds (boolean) | ✅ | ✅ | `.gt()`, `.lt()` |
|
|
1532
|
+
| Exclusive bounds (number) | ❌ | ✅ | `.gt()`, `.lt()` |
|
|
1533
|
+
| multipleOf | ✅ | ✅ | `.multipleOf()` |
|
|
1534
|
+
| Array constraints | ✅ | ✅ | `.min()`, `.max()` |
|
|
1535
|
+
| uniqueItems | ✅ | ✅ | `.refine()` with Set |
|
|
1536
|
+
| prefixItems (tuples) | ❌ | ✅ | `z.tuple()` |
|
|
1537
|
+
| additionalProperties | ✅ | ✅ | `.strict()`, `.catchall()` |
|
|
1538
|
+
| minProperties/maxProperties | ✅ | ✅ | `.refine()` |
|
|
1539
|
+
| const | ✅ | ✅ | `z.literal()` |
|
|
1540
|
+
| nullable (property) | ✅ | ✅ | `.nullable()` |
|
|
1541
|
+
| nullable (type array) | ❌ | ✅ | `.nullable()` |
|
|
1542
|
+
| allOf (objects) | ✅ | ✅ | `.extend()` |
|
|
1543
|
+
| allOf (primitives) | ✅ | ✅ | `.and()` |
|
|
1544
|
+
| oneOf/anyOf | ✅ | ✅ | `z.union()` |
|
|
1545
|
+
| discriminators | ✅ | ✅ | `z.discriminatedUnion()` |
|
|
1546
|
+
| deprecated | ✅ | ✅ | JSDoc `@deprecated` |
|
|
1547
|
+
| title | ✅ | ✅ | JSDoc comment |
|
|
1548
|
+
| examples | ✅ | ✅ | JSDoc `@example` |
|
|
1549
|
+
| format | ✅ | ✅ | Specific Zod validators |
|
|
1550
|
+
| readOnly/writeOnly | ✅ | ✅ | Schema filtering |
|
|
1551
|
+
|
|
1552
|
+
## Error Messages
|
|
1553
|
+
|
|
1554
|
+
The generator provides clear, actionable error messages:
|
|
1555
|
+
|
|
1556
|
+
### Invalid References
|
|
1557
|
+
|
|
1558
|
+
```
|
|
1559
|
+
Error: Invalid schema 'User': Invalid reference at 'profile':
|
|
1560
|
+
'#/components/schemas/NonExistentProfile' points to non-existent schema 'NonExistentProfile'
|
|
1561
|
+
```
|
|
1562
|
+
|
|
1563
|
+
### YAML Syntax Errors
|
|
1564
|
+
|
|
1565
|
+
```
|
|
1566
|
+
Error: Failed to parse OpenAPI YAML file at openapi.yaml:
|
|
1567
|
+
Implicit keys need to be on a single line at line 12, column 9
|
|
1568
|
+
```
|
|
1569
|
+
|
|
1570
|
+
All errors include:
|
|
1571
|
+
|
|
1572
|
+
- File path
|
|
1573
|
+
- Line and column numbers (when available)
|
|
1574
|
+
- Clear description of the problem
|
|
1575
|
+
- Context about what was expected
|
|
1576
|
+
|
|
1577
|
+
## Public Utility Exports
|
|
1578
|
+
|
|
1579
|
+
Starting from **v0.7.0**, this package exports several utilities that can be used by other packages (like `@spartaficial/openapi-to-zod-playwright`):
|
|
1580
|
+
|
|
1581
|
+
### `LRUCache<K, V>`
|
|
1582
|
+
|
|
1583
|
+
A Least Recently Used (LRU) cache implementation for efficient caching.
|
|
1584
|
+
|
|
1585
|
+
```typescript
|
|
1586
|
+
import { LRUCache } from "@spartaficial/openapi-to-zod";
|
|
1587
|
+
|
|
1588
|
+
const cache = new LRUCache<string, ParsedSpec>(50);
|
|
1589
|
+
cache.set("spec-key", parsedSpec);
|
|
1590
|
+
const spec = cache.get("spec-key");
|
|
1591
|
+
```
|
|
1592
|
+
|
|
1593
|
+
### `toPascalCase(str: string | number): string`
|
|
1594
|
+
|
|
1595
|
+
Converts strings to PascalCase, handling kebab-case, snake_case, and special characters.
|
|
1596
|
+
|
|
1597
|
+
```typescript
|
|
1598
|
+
import { toPascalCase } from "@spartaficial/openapi-to-zod";
|
|
1599
|
+
|
|
1600
|
+
toPascalCase("my-api-client"); // => 'MyApiClient'
|
|
1601
|
+
toPascalCase("user_name"); // => 'UserName'
|
|
1602
|
+
```
|
|
1603
|
+
|
|
1604
|
+
### `escapeJSDoc(str: string): string`
|
|
1605
|
+
|
|
1606
|
+
Escapes JSDoc comment terminators to prevent injection.
|
|
1607
|
+
|
|
1608
|
+
```typescript
|
|
1609
|
+
import { escapeJSDoc } from "@spartaficial/openapi-to-zod";
|
|
1610
|
+
|
|
1611
|
+
escapeJSDoc("Comment with */ terminator"); // => 'Comment with *\\/ terminator'
|
|
1612
|
+
```
|
|
1613
|
+
|
|
1614
|
+
### `executeBatch<T>()` and `Generator` Interface
|
|
1615
|
+
|
|
1616
|
+
Execute batch processing with custom generators.
|
|
1617
|
+
|
|
1618
|
+
```typescript
|
|
1619
|
+
import { executeBatch, type Generator } from "@spartaficial/openapi-to-zod";
|
|
1620
|
+
|
|
1621
|
+
class MyGenerator implements Generator {
|
|
1622
|
+
generate(): void {
|
|
1623
|
+
// Your generation logic
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
await executeBatch(
|
|
1628
|
+
specs,
|
|
1629
|
+
"sequential", // or 'parallel'
|
|
1630
|
+
spec => new MyGenerator(spec)
|
|
1631
|
+
);
|
|
1632
|
+
```
|
|
1633
|
+
|
|
1634
|
+
### Config Validation Utilities
|
|
1635
|
+
|
|
1636
|
+
Shared utilities for configuration file validation:
|
|
1637
|
+
|
|
1638
|
+
```typescript
|
|
1639
|
+
import {
|
|
1640
|
+
createTypeScriptLoader,
|
|
1641
|
+
formatConfigValidationError,
|
|
1642
|
+
type RequestResponseOptions,
|
|
1643
|
+
type BaseOperationFilters,
|
|
1644
|
+
} from "@spartaficial/openapi-to-zod";
|
|
1645
|
+
|
|
1646
|
+
// Create TypeScript config loader for cosmiconfig
|
|
1647
|
+
const loader = createTypeScriptLoader();
|
|
1648
|
+
|
|
1649
|
+
// Format Zod validation errors
|
|
1650
|
+
const errorMessage = formatConfigValidationError(zodError, filePath, configPath, [
|
|
1651
|
+
"Additional note 1",
|
|
1652
|
+
"Additional note 2",
|
|
1653
|
+
]);
|
|
1654
|
+
```
|
|
1655
|
+
|
|
1656
|
+
These utilities are marked with `@shared` tags in the source code and are covered by comprehensive tests.
|
|
1657
|
+
|
|
1658
|
+
## API Reference
|
|
1659
|
+
|
|
1660
|
+
### `OpenApiGenerator`
|
|
1661
|
+
|
|
1662
|
+
Main class for generating Zod schemas from OpenAPI specifications.
|
|
1663
|
+
|
|
1664
|
+
```typescript
|
|
1665
|
+
import { OpenApiGenerator } from "@spartaficial/openapi-to-zod";
|
|
1666
|
+
|
|
1667
|
+
const generator = new OpenApiGenerator(options);
|
|
1668
|
+
|
|
1669
|
+
// Generate and write to file
|
|
1670
|
+
generator.generate();
|
|
1671
|
+
|
|
1672
|
+
// Or generate as string
|
|
1673
|
+
const code = generator.generateString();
|
|
1674
|
+
```
|
|
1675
|
+
|
|
1676
|
+
#### Options
|
|
1677
|
+
|
|
1678
|
+
```typescript
|
|
1679
|
+
interface OpenApiGeneratorOptions {
|
|
1680
|
+
/**
|
|
1681
|
+
* Input OpenAPI YAML/JSON file path
|
|
1682
|
+
*/
|
|
1683
|
+
input: string;
|
|
1684
|
+
|
|
1685
|
+
/**
|
|
1686
|
+
* Output TypeScript file path
|
|
1687
|
+
*/
|
|
1688
|
+
outputTypes: string;
|
|
1689
|
+
|
|
1690
|
+
/**
|
|
1691
|
+
* Object validation mode
|
|
1692
|
+
* - 'strict': Uses z.strictObject() - no additional properties allowed
|
|
1693
|
+
* - 'normal': Uses z.object() - additional properties allowed
|
|
1694
|
+
* - 'loose': Uses z.looseObject() - explicitly allows additional properties
|
|
1695
|
+
*
|
|
1696
|
+
* @default "strict"
|
|
1697
|
+
*/
|
|
1698
|
+
mode?: "strict" | "normal" | "loose";
|
|
1699
|
+
|
|
1700
|
+
/**
|
|
1701
|
+
* Whether to include descriptions as JSDoc comments
|
|
1702
|
+
*/
|
|
1703
|
+
includeDescriptions?: boolean;
|
|
1704
|
+
|
|
1705
|
+
/**
|
|
1706
|
+
* Add custom prefix to schema names
|
|
1707
|
+
*/
|
|
1708
|
+
prefix?: string;
|
|
1709
|
+
|
|
1710
|
+
/**
|
|
1711
|
+
* Add custom suffix to schema names
|
|
1712
|
+
*/
|
|
1713
|
+
suffix?: string;
|
|
1714
|
+
|
|
1715
|
+
/**
|
|
1716
|
+
* Strip prefix from schema names using glob patterns
|
|
1717
|
+
*/
|
|
1718
|
+
stripSchemaPrefix?: string | string[];
|
|
1719
|
+
|
|
1720
|
+
/**
|
|
1721
|
+
* Show generation statistics in output
|
|
1722
|
+
*/
|
|
1723
|
+
showStats?: boolean;
|
|
1724
|
+
|
|
1725
|
+
/**
|
|
1726
|
+
* Schema filtering mode
|
|
1727
|
+
*/
|
|
1728
|
+
schemaType?: "all" | "request" | "response";
|
|
1729
|
+
|
|
1730
|
+
/**
|
|
1731
|
+
* Operation filters for including/excluding operations
|
|
1732
|
+
*/
|
|
1733
|
+
operationFilters?: OperationFilters;
|
|
1734
|
+
}
|
|
1735
|
+
```
|
|
1736
|
+
|
|
1737
|
+
### `defineConfig`
|
|
1738
|
+
|
|
1739
|
+
Type-safe helper for creating configuration files.
|
|
1740
|
+
|
|
1741
|
+
```typescript
|
|
1742
|
+
import { defineConfig } from "@spartaficial/openapi-to-zod";
|
|
1743
|
+
|
|
1744
|
+
export default defineConfig({
|
|
1745
|
+
specs: [{ input: "api.yaml", outputTypes: "schemas.ts" }],
|
|
1746
|
+
});
|
|
1747
|
+
```
|
|
1748
|
+
|
|
1749
|
+
## Requirements
|
|
1750
|
+
|
|
1751
|
+
- Node.js 22 (22.18 or later) or Node.js 24 and later
|
|
1752
|
+
- Zod ^4
|
|
1753
|
+
|
|
1754
|
+
## Test Coverage
|
|
1755
|
+
|
|
1756
|
+
Comprehensive test suite with **364 passing tests** covering:
|
|
1757
|
+
|
|
1758
|
+
- ✅ **Basic Schema Generation** (14 tests) - Core OpenAPI types, references, nested objects
|
|
1759
|
+
- ✅ **Enum Generation** (4 tests) - Zod enum generation and handling
|
|
1760
|
+
- ✅ **Circular References** (5 tests) - Self-references, mutual references, validation
|
|
1761
|
+
- ✅ **Format Support** (9 tests) - UUID, email, URL, date-time, and 15+ other formats
|
|
1762
|
+
- ✅ **Validation Modes** (7 tests) - Strict, normal, loose object validation
|
|
1763
|
+
- ✅ **CLI Integration** (7 tests) - Command-line interface, options parsing
|
|
1764
|
+
- ✅ **Integration Tests** (6 tests) - End-to-end schema generation
|
|
1765
|
+
- ✅ **Constraint Features** (12 tests) - additionalProperties, multipleOf, array constraints
|
|
1766
|
+
- ✅ **Additional Features** (22 tests) - minProperties/maxProperties, OpenAPI 3.1 nullable, title/examples
|
|
1767
|
+
- ✅ **New Features** (18 tests) - const, uniqueItems, exclusive bounds, deprecated
|
|
1768
|
+
- ✅ **Tuples & AllOf** (19 tests) - prefixItems, improved object merging
|
|
1769
|
+
- ✅ **Request/Response Schemas** - readOnly/writeOnly filtering
|
|
1770
|
+
- ✅ **Discriminated Unions** - oneOf/anyOf with discriminators
|
|
1771
|
+
- ✅ **Batch Processing** - Config file loading and parallel execution
|
|
1772
|
+
|
|
1773
|
+
## Licensing
|
|
1774
|
+
|
|
1775
|
+
This package is commercial software. **Generation requires a license key.**
|
|
1776
|
+
|
|
1777
|
+
```bash
|
|
1778
|
+
SPARTAFICIAL_LICENSE_KEY=<your key>
|
|
1779
|
+
```
|
|
1780
|
+
|
|
1781
|
+
One key covers every `@spartaficial` package in every project you own. It names your
|
|
1782
|
+
organisation and an expiry date; set the same value wherever the generators run. A key can
|
|
1783
|
+
optionally be restricted to a single project by its `package.json` name — only then does
|
|
1784
|
+
the directory you run from matter, and it is resolved from the directory holding your
|
|
1785
|
+
codegen config, so running the CLI from a monorepo root still binds to the right workspace.
|
|
1786
|
+
|
|
1787
|
+
`--help`, `--version` and `init` run without a key.
|
|
1788
|
+
|
|
1789
|
+
**Your build is never gated.** Compiling code that was already generated, and running
|
|
1790
|
+
tests against the generated schemas, work forever with no key set. If a license lapses,
|
|
1791
|
+
re-running codegen fails while your application keeps building and shipping. Generated
|
|
1792
|
+
files contain no license logic.
|
|
1793
|
+
|
|
1794
|
+
See the [licensing documentation](https://spartaficial.dev/docs/openapi-codegen/licensing)
|
|
1795
|
+
for the error-code table and CI setup.
|
|
1796
|
+
|
|
1797
|
+
To buy or renew a key, contact Cerios - Spartaficial at spartaficial@cerios.com.
|
|
1798
|
+
|
|
1799
|
+
## License
|
|
1800
|
+
|
|
1801
|
+
Spartaficial Commercial License — see [LICENSE](./LICENSE).
|
|
1802
|
+
|
|
1803
|
+
Code generated by this tool is yours, with no restriction and no royalty.
|
|
1804
|
+
|
|
1805
|
+
Copyright © 2026 Spartaficial - Cerios
|
|
1806
|
+
|
|
1807
|
+
## Contributing
|
|
1808
|
+
|
|
1809
|
+
This is proprietary software and we do not accept external pull requests.
|
|
1810
|
+
Bug reports and feature requests are welcome on the
|
|
1811
|
+
[GitHub issues](https://github.com/Spartaficial/openapi-codegen-support/issues) page.
|
|
1812
|
+
|
|
1813
|
+
## Support
|
|
1814
|
+
|
|
1815
|
+
For issues and questions, please use the [GitHub issues](https://github.com/Spartaficial/openapi-codegen-support/issues) page.
|