@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/README.md CHANGED
@@ -1,3 +1,1815 @@
1
- # Temporary Holding Version
1
+ # @spartaficial/openapi-to-zod
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.