@cerios/openapi-to-zod 1.1.1 → 1.3.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 +192 -10
- package/dist/cli.js +421 -49
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +428 -50
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +13 -2
- package/dist/index.d.ts +13 -2
- package/dist/index.js +384 -46
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +384 -46
- package/dist/index.mjs.map +1 -1
- package/dist/internal.d.mts +72 -2
- package/dist/internal.d.ts +72 -2
- package/dist/internal.js +139 -1
- package/dist/internal.js.map +1 -1
- package/dist/internal.mjs +132 -1
- package/dist/internal.mjs.map +1 -1
- package/dist/{types-CI48CjiU.d.mts → types-B3GgqGzM.d.mts} +139 -1
- package/dist/{types-CI48CjiU.d.ts → types-B3GgqGzM.d.ts} +139 -1
- package/package.json +16 -4
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ Transform OpenAPI YAML specifications into Zod v4 compliant schemas with full Ty
|
|
|
19
19
|
- 📊 **Statistics**: Optional generation statistics in output files
|
|
20
20
|
- ❗ **Better Errors**: Clear error messages with file paths and line numbers
|
|
21
21
|
- 🎭 **Tuple Validation**: OpenAPI 3.1 `prefixItems` support with `.tuple()` and `.rest()`
|
|
22
|
-
- 🔗 **Smart AllOf**: Uses `.
|
|
22
|
+
- 🔗 **Smart AllOf**: Uses `.extend()` for objects (Zod v4), `.and()` for primitives
|
|
23
23
|
- 🎯 **Literal Types**: `const` keyword support with `z.literal()`
|
|
24
24
|
- 🔢 **Exclusive Bounds**: `exclusiveMinimum`/`exclusiveMaximum` with `.gt()`/`.lt()`
|
|
25
25
|
- 🎨 **Unique Arrays**: `uniqueItems` validation with Set-based checking
|
|
@@ -181,9 +181,11 @@ Examples:
|
|
|
181
181
|
| `name` | `string` | Optional identifier for logging |
|
|
182
182
|
| `input` | `string` | Input OpenAPI YAML file path (required) |
|
|
183
183
|
| `output` | `string` | Output TypeScript file path (required) |
|
|
184
|
-
| `mode` | `"strict"` \| `"normal"` \| `"loose"` | Validation mode |
|
|
184
|
+
| `mode` | `"strict"` \| `"normal"` \| `"loose"` | Validation mode for top-level schemas (default: `"normal"`) |
|
|
185
|
+
| `emptyObjectBehavior` | `"strict"` \| `"loose"` \| `"record"` | How to handle empty objects (default: `"loose"`) |
|
|
185
186
|
| `includeDescriptions` | `boolean` | Include JSDoc comments |
|
|
186
187
|
| `useDescribe` | `boolean` | Add `.describe()` calls |
|
|
188
|
+
| `defaultNullable` | `boolean` | Treat properties as nullable by default when not explicitly specified (default: `false`) |
|
|
187
189
|
| `schemaType` | `"all"` \| `"request"` \| `"response"` | Schema filtering |
|
|
188
190
|
| `prefix` | `string` | Prefix for schema names |
|
|
189
191
|
| `suffix` | `string` | Suffix for schema names |
|
|
@@ -313,6 +315,43 @@ const userSchema = z.looseObject({
|
|
|
313
315
|
});
|
|
314
316
|
```
|
|
315
317
|
|
|
318
|
+
## Empty Object Behavior
|
|
319
|
+
|
|
320
|
+
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:
|
|
321
|
+
|
|
322
|
+
### Loose (default)
|
|
323
|
+
Uses `z.looseObject({})` which allows any additional properties:
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
// OpenAPI: { type: object }
|
|
327
|
+
const metadataSchema = z.looseObject({});
|
|
328
|
+
|
|
329
|
+
// Accepts: {}, { foo: "bar" }, { any: "properties" }
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Strict
|
|
333
|
+
Uses `z.strictObject({})` which rejects any properties:
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
// OpenAPI: { type: object }
|
|
337
|
+
const emptySchema = z.strictObject({});
|
|
338
|
+
|
|
339
|
+
// Accepts: {}
|
|
340
|
+
// Rejects: { foo: "bar" }
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Record
|
|
344
|
+
Uses `z.record(z.string(), z.unknown())` which treats it as an arbitrary key-value map:
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
// OpenAPI: { type: object }
|
|
348
|
+
const mapSchema = z.record(z.string(), z.unknown());
|
|
349
|
+
|
|
350
|
+
// Accepts: {}, { foo: "bar" }, { any: "properties" }
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
> **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.
|
|
354
|
+
|
|
316
355
|
## Examples
|
|
317
356
|
|
|
318
357
|
### Input OpenAPI YAML
|
|
@@ -410,6 +449,68 @@ The generator supports all OpenAPI string formats with Zod v4:
|
|
|
410
449
|
| `cidrv4` | `z.cidrv4()` |
|
|
411
450
|
| `cidrv6` | `z.cidrv6()` |
|
|
412
451
|
|
|
452
|
+
### Custom Date-Time Format
|
|
453
|
+
|
|
454
|
+
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`).
|
|
455
|
+
|
|
456
|
+
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:
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
import { defineConfig } from '@cerios/openapi-to-zod';
|
|
460
|
+
|
|
461
|
+
export default defineConfig({
|
|
462
|
+
defaults: {
|
|
463
|
+
// For date-times without Z suffix
|
|
464
|
+
customDateTimeFormatRegex: '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$',
|
|
465
|
+
},
|
|
466
|
+
specs: [
|
|
467
|
+
{
|
|
468
|
+
input: 'openapi.yaml',
|
|
469
|
+
output: 'src/schemas.ts',
|
|
470
|
+
},
|
|
471
|
+
],
|
|
472
|
+
});
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
**TypeScript Config - RegExp Literals:**
|
|
476
|
+
|
|
477
|
+
In TypeScript config files, you can also use RegExp literals (which don't require double-escaping):
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
export default defineConfig({
|
|
481
|
+
defaults: {
|
|
482
|
+
// Use RegExp literal (single escaping)
|
|
483
|
+
customDateTimeFormatRegex: /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/,
|
|
484
|
+
},
|
|
485
|
+
specs: [
|
|
486
|
+
{
|
|
487
|
+
input: 'openapi.yaml',
|
|
488
|
+
output: 'src/schemas.ts',
|
|
489
|
+
},
|
|
490
|
+
],
|
|
491
|
+
});
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
**Common Custom Formats:**
|
|
495
|
+
|
|
496
|
+
| Use Case | String Pattern (JSON/YAML) | RegExp Literal (TypeScript) |
|
|
497
|
+
|----------|----------------------------|----------------------------|
|
|
498
|
+
| 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}$/` |
|
|
499
|
+
| 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}$/` |
|
|
500
|
+
| 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?$/` |
|
|
501
|
+
| 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?$/` |
|
|
502
|
+
|
|
503
|
+
**Generated Output:**
|
|
504
|
+
|
|
505
|
+
When using a custom regex, the generator will produce:
|
|
506
|
+
|
|
507
|
+
```typescript
|
|
508
|
+
// Instead of: z.iso.datetime()
|
|
509
|
+
// You get: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/)
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
**Note:** This option only affects `date-time` format fields. Other formats (like `date`, `email`, `uuid`) remain unchanged.
|
|
513
|
+
|
|
413
514
|
## Advanced Features
|
|
414
515
|
|
|
415
516
|
### Operation Filtering
|
|
@@ -563,9 +664,90 @@ export const userSchema = z.object({
|
|
|
563
664
|
|
|
564
665
|
OpenAPI's `nullable: true` is converted to `.nullable()`
|
|
565
666
|
|
|
667
|
+
#### Default Nullable Behavior
|
|
668
|
+
|
|
669
|
+
By default, properties are only nullable when explicitly marked with `nullable: true` (OpenAPI 3.0) or `type: ["string", "null"]` (OpenAPI 3.1).
|
|
670
|
+
|
|
671
|
+
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:
|
|
672
|
+
|
|
673
|
+
```typescript
|
|
674
|
+
export default defineConfig({
|
|
675
|
+
specs: [{
|
|
676
|
+
input: 'openapi.yaml',
|
|
677
|
+
output: 'schemas.ts',
|
|
678
|
+
defaultNullable: true, // Treat unspecified properties as nullable
|
|
679
|
+
}]
|
|
680
|
+
});
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
**Important:** `defaultNullable` only applies to **primitive property values** within objects. It does NOT apply to:
|
|
684
|
+
|
|
685
|
+
- **Top-level schema definitions** - Schemas are not made nullable at the definition level
|
|
686
|
+
- **Schema references (`$ref`)** - References preserve the nullability of the target schema; add explicit `nullable: true` if needed
|
|
687
|
+
- **Enum values** - Enums define discrete values and are not nullable by default
|
|
688
|
+
- **Const/literal values** - Literals are exact values and are not nullable by default
|
|
689
|
+
|
|
690
|
+
**Behavior comparison:**
|
|
691
|
+
|
|
692
|
+
| Schema Property | `defaultNullable: false` (default) | `defaultNullable: true` |
|
|
693
|
+
|-----------------|-------------------------------------|-------------------------|
|
|
694
|
+
| `nullable: true` | `.nullable()` | `.nullable()` |
|
|
695
|
+
| `nullable: false` | No `.nullable()` | No `.nullable()` |
|
|
696
|
+
| No annotation (primitive) | No `.nullable()` | `.nullable()` |
|
|
697
|
+
| No annotation (`$ref`) | No `.nullable()` | No `.nullable()` |
|
|
698
|
+
| No annotation (enum) | No `.nullable()` | No `.nullable()` |
|
|
699
|
+
| No annotation (const) | No `.nullable()` | No `.nullable()` |
|
|
700
|
+
|
|
701
|
+
**Example:**
|
|
702
|
+
|
|
703
|
+
```yaml
|
|
704
|
+
components:
|
|
705
|
+
schemas:
|
|
706
|
+
Status:
|
|
707
|
+
type: string
|
|
708
|
+
enum: [active, inactive]
|
|
709
|
+
User:
|
|
710
|
+
type: object
|
|
711
|
+
properties:
|
|
712
|
+
id:
|
|
713
|
+
type: integer
|
|
714
|
+
name:
|
|
715
|
+
type: string
|
|
716
|
+
status:
|
|
717
|
+
$ref: '#/components/schemas/Status'
|
|
718
|
+
nullableStatus:
|
|
719
|
+
allOf:
|
|
720
|
+
- $ref: '#/components/schemas/Status'
|
|
721
|
+
nullable: true
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
**With `defaultNullable: false` (default):**
|
|
725
|
+
```typescript
|
|
726
|
+
export const statusSchema = z.enum(["active", "inactive"]);
|
|
727
|
+
|
|
728
|
+
export const userSchema = z.object({
|
|
729
|
+
id: z.number().int(),
|
|
730
|
+
name: z.string(), // Not nullable (no annotation)
|
|
731
|
+
status: statusSchema, // Not nullable ($ref)
|
|
732
|
+
nullableStatus: statusSchema.nullable(), // Explicitly nullable
|
|
733
|
+
});
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
**With `defaultNullable: true`:**
|
|
737
|
+
```typescript
|
|
738
|
+
export const statusSchema = z.enum(["active", "inactive"]);
|
|
739
|
+
|
|
740
|
+
export const userSchema = z.object({
|
|
741
|
+
id: z.number().int().nullable(), // Nullable (primitive)
|
|
742
|
+
name: z.string().nullable(), // Nullable (primitive)
|
|
743
|
+
status: statusSchema, // NOT nullable ($ref - must be explicit)
|
|
744
|
+
nullableStatus: statusSchema.nullable(), // Explicitly nullable
|
|
745
|
+
});
|
|
746
|
+
```
|
|
747
|
+
|
|
566
748
|
### Schema Composition
|
|
567
749
|
|
|
568
|
-
- `allOf` → `.
|
|
750
|
+
- `allOf` → `.extend()` for objects (Zod v4), `.and()` for primitives
|
|
569
751
|
- `oneOf`, `anyOf` → `z.union()` or `z.discriminatedUnion()`
|
|
570
752
|
- `$ref` → Proper schema references
|
|
571
753
|
|
|
@@ -1015,11 +1197,11 @@ export const flexibleMetadataSchema = z
|
|
|
1015
1197
|
|
|
1016
1198
|
### Schema Composition
|
|
1017
1199
|
|
|
1018
|
-
#### AllOf - Smart
|
|
1200
|
+
#### AllOf - Smart Extending
|
|
1019
1201
|
|
|
1020
|
-
Uses `.
|
|
1202
|
+
Uses `.extend()` for objects (Zod v4 compliant - `.merge()` is deprecated), `.and()` for primitives:
|
|
1021
1203
|
|
|
1022
|
-
**Object
|
|
1204
|
+
**Object Extending:**
|
|
1023
1205
|
```yaml
|
|
1024
1206
|
User:
|
|
1025
1207
|
allOf:
|
|
@@ -1036,10 +1218,10 @@ User:
|
|
|
1036
1218
|
**Generated:**
|
|
1037
1219
|
```typescript
|
|
1038
1220
|
export const userSchema = baseEntitySchema
|
|
1039
|
-
.
|
|
1040
|
-
.
|
|
1221
|
+
.extend(timestampedSchema.shape)
|
|
1222
|
+
.extend(z.object({
|
|
1041
1223
|
username: z.string()
|
|
1042
|
-
}));
|
|
1224
|
+
}).shape);
|
|
1043
1225
|
```
|
|
1044
1226
|
|
|
1045
1227
|
#### OneOf / AnyOf
|
|
@@ -1161,7 +1343,7 @@ export const statusCodeSchema = z.enum(["200", "201", "400", "404", "500"]);
|
|
|
1161
1343
|
| const | ✅ | ✅ | `z.literal()` |
|
|
1162
1344
|
| nullable (property) | ✅ | ✅ | `.nullable()` |
|
|
1163
1345
|
| nullable (type array) | ❌ | ✅ | `.nullable()` |
|
|
1164
|
-
| allOf (objects) | ✅ | ✅ | `.
|
|
1346
|
+
| allOf (objects) | ✅ | ✅ | `.extend()` |
|
|
1165
1347
|
| allOf (primitives) | ✅ | ✅ | `.and()` |
|
|
1166
1348
|
| oneOf/anyOf | ✅ | ✅ | `z.union()` |
|
|
1167
1349
|
| discriminators | ✅ | ✅ | `z.discriminatedUnion()` |
|