@cerios/openapi-to-zod 1.2.0 → 1.3.1

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
@@ -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 `.merge()` for objects, `.and()` for primitives
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,7 +181,8 @@ 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 |
187
188
  | `defaultNullable` | `boolean` | Treat properties as nullable by default when not explicitly specified (default: `false`) |
@@ -314,6 +315,43 @@ const userSchema = z.looseObject({
314
315
  });
315
316
  ```
316
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
+
317
355
  ## Examples
318
356
 
319
357
  ### Input OpenAPI YAML
@@ -642,55 +680,74 @@ export default defineConfig({
642
680
  });
643
681
  ```
644
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
+
645
690
  **Behavior comparison:**
646
691
 
647
692
  | Schema Property | `defaultNullable: false` (default) | `defaultNullable: true` |
648
693
  |-----------------|-------------------------------------|-------------------------|
649
694
  | `nullable: true` | `.nullable()` | `.nullable()` |
650
695
  | `nullable: false` | No `.nullable()` | No `.nullable()` |
651
- | No `nullable` specified | No `.nullable()` | `.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()` |
652
700
 
653
701
  **Example:**
654
702
 
655
703
  ```yaml
656
- User:
657
- type: object
658
- properties:
659
- id:
660
- type: integer
661
- name:
662
- type: string
663
- email:
664
- type: string
665
- nullable: true
666
- phone:
704
+ components:
705
+ schemas:
706
+ Status:
667
707
  type: string
668
- nullable: false
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
669
722
  ```
670
723
 
671
724
  **With `defaultNullable: false` (default):**
672
725
  ```typescript
726
+ export const statusSchema = z.enum(["active", "inactive"]);
727
+
673
728
  export const userSchema = z.object({
674
729
  id: z.number().int(),
675
- name: z.string(), // Not nullable (no annotation)
676
- email: z.string().nullable(), // Explicitly nullable
677
- phone: z.string(), // Explicitly not nullable
730
+ name: z.string(), // Not nullable (no annotation)
731
+ status: statusSchema, // Not nullable ($ref)
732
+ nullableStatus: statusSchema.nullable(), // Explicitly nullable
678
733
  });
679
734
  ```
680
735
 
681
736
  **With `defaultNullable: true`:**
682
737
  ```typescript
738
+ export const statusSchema = z.enum(["active", "inactive"]);
739
+
683
740
  export const userSchema = z.object({
684
- id: z.number().int().nullable(), // Nullable by default
685
- name: z.string().nullable(), // Nullable by default
686
- email: z.string().nullable(), // Explicitly nullable
687
- phone: z.string(), // Explicitly NOT nullable (respected)
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
688
745
  });
689
746
  ```
690
747
 
691
748
  ### Schema Composition
692
749
 
693
- - `allOf` → `.merge()` for objects, `.and()` for primitives
750
+ - `allOf` → `.extend()` for objects (Zod v4), `.and()` for primitives
694
751
  - `oneOf`, `anyOf` → `z.union()` or `z.discriminatedUnion()`
695
752
  - `$ref` → Proper schema references
696
753
 
@@ -1140,11 +1197,11 @@ export const flexibleMetadataSchema = z
1140
1197
 
1141
1198
  ### Schema Composition
1142
1199
 
1143
- #### AllOf - Smart Merging
1200
+ #### AllOf - Smart Extending
1144
1201
 
1145
- Uses `.merge()` for objects, `.and()` for primitives:
1202
+ Uses `.extend()` for objects (Zod v4 compliant - `.merge()` is deprecated), `.and()` for primitives:
1146
1203
 
1147
- **Object Merging:**
1204
+ **Object Extending:**
1148
1205
  ```yaml
1149
1206
  User:
1150
1207
  allOf:
@@ -1161,10 +1218,10 @@ User:
1161
1218
  **Generated:**
1162
1219
  ```typescript
1163
1220
  export const userSchema = baseEntitySchema
1164
- .merge(timestampedSchema)
1165
- .merge(z.object({
1221
+ .extend(timestampedSchema.shape)
1222
+ .extend(z.object({
1166
1223
  username: z.string()
1167
- }));
1224
+ }).shape);
1168
1225
  ```
1169
1226
 
1170
1227
  #### OneOf / AnyOf
@@ -1286,7 +1343,7 @@ export const statusCodeSchema = z.enum(["200", "201", "400", "404", "500"]);
1286
1343
  | const | ✅ | ✅ | `z.literal()` |
1287
1344
  | nullable (property) | ✅ | ✅ | `.nullable()` |
1288
1345
  | nullable (type array) | ❌ | ✅ | `.nullable()` |
1289
- | allOf (objects) | ✅ | ✅ | `.merge()` |
1346
+ | allOf (objects) | ✅ | ✅ | `.extend()` |
1290
1347
  | allOf (primitives) | ✅ | ✅ | `.and()` |
1291
1348
  | oneOf/anyOf | ✅ | ✅ | `z.union()` |
1292
1349
  | discriminators | ✅ | ✅ | `z.discriminatedUnion()` |