@cerios/openapi-to-zod 1.2.0 → 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 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()` |
package/dist/cli.js CHANGED
@@ -5480,12 +5480,56 @@ function generateArrayValidation(schema, context) {
5480
5480
 
5481
5481
  // src/validators/composition-validator.ts
5482
5482
  init_cjs_shims();
5483
+ function isDiscriminatorRequired(schemas, discriminator, context) {
5484
+ const invalidSchemas = [];
5485
+ for (const schema of schemas) {
5486
+ const resolved = resolveSchema(schema, context);
5487
+ const required = resolved.required || [];
5488
+ if (!required.includes(discriminator)) {
5489
+ const schemaName = schema.$ref ? schema.$ref.split("/").pop() || "inline" : "inline";
5490
+ invalidSchemas.push(schemaName);
5491
+ }
5492
+ }
5493
+ return {
5494
+ valid: invalidSchemas.length === 0,
5495
+ invalidSchemas
5496
+ };
5497
+ }
5483
5498
  function generateUnion(schemas, discriminator, isNullable2, context, options, currentSchema) {
5499
+ if (schemas.length === 0) {
5500
+ console.warn(
5501
+ "[openapi-to-zod] Warning: Empty oneOf/anyOf array encountered. This is likely a malformed OpenAPI spec. Generating z.never() as fallback."
5502
+ );
5503
+ return wrapNullable(
5504
+ 'z.never().describe("Empty oneOf/anyOf in OpenAPI spec - no valid schema defined")',
5505
+ isNullable2
5506
+ );
5507
+ }
5508
+ if (schemas.length === 1) {
5509
+ let singleSchema = context.generatePropertySchema(schemas[0], currentSchema);
5510
+ if ((options == null ? void 0 : options.passthrough) && !singleSchema.includes(".catchall(")) {
5511
+ singleSchema = `${singleSchema}.catchall(z.unknown())`;
5512
+ }
5513
+ return wrapNullable(singleSchema, isNullable2);
5514
+ }
5484
5515
  if (discriminator) {
5485
5516
  let resolvedSchemas = schemas;
5486
5517
  if ((options == null ? void 0 : options.discriminatorMapping) && context.resolveDiscriminatorMapping) {
5487
5518
  resolvedSchemas = context.resolveDiscriminatorMapping(options.discriminatorMapping, schemas);
5488
5519
  }
5520
+ const discriminatorCheck = isDiscriminatorRequired(resolvedSchemas, discriminator, context);
5521
+ if (!discriminatorCheck.valid) {
5522
+ console.warn(
5523
+ `[openapi-to-zod] Warning: Discriminator "${discriminator}" is not required in schemas: ${discriminatorCheck.invalidSchemas.join(", ")}. Falling back to z.union() instead of z.discriminatedUnion().`
5524
+ );
5525
+ let schemaStrings3 = resolvedSchemas.map((s) => context.generatePropertySchema(s, currentSchema));
5526
+ if (options == null ? void 0 : options.passthrough) {
5527
+ schemaStrings3 = schemaStrings3.map((s) => s.includes(".catchall(") ? s : `${s}.catchall(z.unknown())`);
5528
+ }
5529
+ const fallbackDescription = `Discriminator "${discriminator}" is optional in some schemas (${discriminatorCheck.invalidSchemas.join(", ")}), using z.union() instead of z.discriminatedUnion()`;
5530
+ const union3 = `z.union([${schemaStrings3.join(", ")}]).describe("${fallbackDescription}")`;
5531
+ return wrapNullable(union3, isNullable2);
5532
+ }
5489
5533
  let schemaStrings2 = resolvedSchemas.map((s) => context.generatePropertySchema(s, currentSchema));
5490
5534
  if (options == null ? void 0 : options.passthrough) {
5491
5535
  schemaStrings2 = schemaStrings2.map((s) => s.includes(".catchall(") ? s : `${s}.catchall(z.unknown())`);
@@ -5500,25 +5544,102 @@ function generateUnion(schemas, discriminator, isNullable2, context, options, cu
5500
5544
  const union = `z.union([${schemaStrings.join(", ")}])`;
5501
5545
  return wrapNullable(union, isNullable2);
5502
5546
  }
5547
+ function resolveSchema(schema, context) {
5548
+ if (schema.$ref && context.resolveSchemaRef) {
5549
+ const resolved = context.resolveSchemaRef(schema.$ref);
5550
+ if (resolved) {
5551
+ return resolved;
5552
+ }
5553
+ }
5554
+ return schema;
5555
+ }
5556
+ function collectProperties(schema, context) {
5557
+ const resolved = resolveSchema(schema, context);
5558
+ const props = /* @__PURE__ */ new Map();
5559
+ const sourceName = schema.$ref ? schema.$ref.split("/").pop() || "unknown" : "inline";
5560
+ if (resolved.properties) {
5561
+ for (const [key, value] of Object.entries(resolved.properties)) {
5562
+ props.set(key, { schema: value, source: sourceName });
5563
+ }
5564
+ }
5565
+ if (resolved.allOf) {
5566
+ for (const subSchema of resolved.allOf) {
5567
+ const subProps = collectProperties(subSchema, context);
5568
+ for (const [key, value] of subProps) {
5569
+ if (!props.has(key)) {
5570
+ props.set(key, value);
5571
+ }
5572
+ }
5573
+ }
5574
+ }
5575
+ return props;
5576
+ }
5577
+ function schemasMatch(a, b) {
5578
+ return JSON.stringify(a) === JSON.stringify(b);
5579
+ }
5580
+ function detectConflictingProperties(schemas, context) {
5581
+ const conflicts = [];
5582
+ const propertyMap = /* @__PURE__ */ new Map();
5583
+ for (const schema of schemas) {
5584
+ const schemaProps = collectProperties(schema, context);
5585
+ for (const [propName, propInfo] of schemaProps) {
5586
+ const existing = propertyMap.get(propName);
5587
+ if (existing) {
5588
+ if (!schemasMatch(existing.schema, propInfo.schema)) {
5589
+ conflicts.push(
5590
+ `Property "${propName}" has conflicting definitions in ${existing.source} and ${propInfo.source}`
5591
+ );
5592
+ }
5593
+ } else {
5594
+ propertyMap.set(propName, propInfo);
5595
+ }
5596
+ }
5597
+ }
5598
+ return conflicts;
5599
+ }
5503
5600
  function generateAllOf(schemas, isNullable2, context, currentSchema) {
5504
5601
  if (schemas.length === 1) {
5505
5602
  const singleSchema = context.generatePropertySchema(schemas[0], currentSchema, false);
5506
5603
  return wrapNullable(singleSchema, isNullable2);
5507
5604
  }
5605
+ const conflicts = detectConflictingProperties(schemas, context);
5606
+ let conflictDescription = "";
5607
+ if (conflicts.length > 0) {
5608
+ for (const conflict of conflicts) {
5609
+ console.warn(`[openapi-to-zod] Warning: allOf composition conflict - ${conflict}`);
5610
+ }
5611
+ conflictDescription = `allOf property conflicts detected: ${conflicts.join("; ")}`;
5612
+ }
5508
5613
  const allObjects = schemas.every((s) => s.type === "object" || s.properties || s.$ref || s.allOf);
5509
- const schemaStrings = schemas.map((s) => context.generatePropertySchema(s, currentSchema, false));
5614
+ let result;
5510
5615
  if (allObjects) {
5511
- let merged2 = schemaStrings[0];
5616
+ let merged = context.generatePropertySchema(schemas[0], currentSchema, false);
5617
+ for (let i = 1; i < schemas.length; i++) {
5618
+ const schema = schemas[i];
5619
+ if (schema.$ref) {
5620
+ const refSchema = context.generatePropertySchema(schema, currentSchema, false);
5621
+ merged = `${merged}.extend(${refSchema}.shape)`;
5622
+ } else if (context.generateInlineObjectShape && (schema.properties || schema.type === "object")) {
5623
+ const inlineShape = context.generateInlineObjectShape(schema, currentSchema);
5624
+ merged = `${merged}.extend(${inlineShape})`;
5625
+ } else {
5626
+ const schemaString = context.generatePropertySchema(schema, currentSchema, false);
5627
+ merged = `${merged}.extend(${schemaString}.shape)`;
5628
+ }
5629
+ }
5630
+ result = merged;
5631
+ } else {
5632
+ const schemaStrings = schemas.map((s) => context.generatePropertySchema(s, currentSchema, false));
5633
+ let merged = schemaStrings[0];
5512
5634
  for (let i = 1; i < schemaStrings.length; i++) {
5513
- merged2 = `${merged2}.merge(${schemaStrings[i]})`;
5635
+ merged = `${merged}.and(${schemaStrings[i]})`;
5514
5636
  }
5515
- return wrapNullable(merged2, isNullable2);
5637
+ result = merged;
5516
5638
  }
5517
- let merged = schemaStrings[0];
5518
- for (let i = 1; i < schemaStrings.length; i++) {
5519
- merged = `${merged}.and(${schemaStrings[i]})`;
5639
+ if (conflictDescription) {
5640
+ result = `${result}.describe("${conflictDescription}")`;
5520
5641
  }
5521
- return wrapNullable(merged, isNullable2);
5642
+ return wrapNullable(result, isNullable2);
5522
5643
  }
5523
5644
 
5524
5645
  // src/validators/number-validator.ts
@@ -6200,6 +6321,15 @@ var _PropertyGenerator = class _PropertyGenerator {
6200
6321
  }
6201
6322
  return mappedSchemas;
6202
6323
  }
6324
+ /**
6325
+ * Resolve a $ref string to the actual schema
6326
+ */
6327
+ resolveSchemaRef(ref) {
6328
+ var _a, _b;
6329
+ const schemaName = ref.split("/").pop();
6330
+ if (!schemaName) return void 0;
6331
+ return (_b = (_a = this.context.spec.components) == null ? void 0 : _a.schemas) == null ? void 0 : _b[schemaName];
6332
+ }
6203
6333
  /**
6204
6334
  * Resolve a schema name through any aliases to get the actual schema name
6205
6335
  * If the schema is an alias (allOf with single $ref), return the target name
@@ -6277,7 +6407,7 @@ var _PropertyGenerator = class _PropertyGenerator {
6277
6407
  let schemaWithCatchall = baseSchema;
6278
6408
  if (baseSchema.includes(".union([") || baseSchema.includes(".discriminatedUnion(")) {
6279
6409
  schemaWithCatchall = baseSchema;
6280
- } else if (baseSchema.includes(".merge(")) {
6410
+ } else if (baseSchema.includes(".extend(")) {
6281
6411
  schemaWithCatchall = `${baseSchema}.catchall(z.unknown())`;
6282
6412
  }
6283
6413
  if (schema.unevaluatedProperties === false) {
@@ -6304,7 +6434,11 @@ var _PropertyGenerator = class _PropertyGenerator {
6304
6434
  if ((this.context.schemaType === "request" || this.context.schemaType === "response") && schema.properties) {
6305
6435
  schema = this.filterNestedProperties(schema);
6306
6436
  }
6307
- const effectiveDefaultNullable = isTopLevel ? false : this.context.defaultNullable;
6437
+ const isSchemaRef = !!schema.$ref;
6438
+ const isEnum = !!schema.enum;
6439
+ const isConst = schema.const !== void 0;
6440
+ const shouldApplyDefaultNullable = !isTopLevel && !isSchemaRef && !isEnum && !isConst;
6441
+ const effectiveDefaultNullable = shouldApplyDefaultNullable ? this.context.defaultNullable : false;
6308
6442
  const nullable = isNullable(schema, effectiveDefaultNullable);
6309
6443
  if (hasMultipleTypes(schema)) {
6310
6444
  const union = this.generateMultiTypeUnion(schema, currentSchema);
@@ -6357,7 +6491,11 @@ var _PropertyGenerator = class _PropertyGenerator {
6357
6491
  let composition = generateAllOf(
6358
6492
  schema.allOf,
6359
6493
  nullable,
6360
- { generatePropertySchema: this.generatePropertySchema.bind(this) },
6494
+ {
6495
+ generatePropertySchema: this.generatePropertySchema.bind(this),
6496
+ generateInlineObjectShape: this.generateInlineObjectShape.bind(this),
6497
+ resolveSchemaRef: this.resolveSchemaRef.bind(this)
6498
+ },
6361
6499
  currentSchema
6362
6500
  );
6363
6501
  if (schema.unevaluatedProperties !== void 0) {
@@ -6373,7 +6511,8 @@ var _PropertyGenerator = class _PropertyGenerator {
6373
6511
  nullable,
6374
6512
  {
6375
6513
  generatePropertySchema: this.generatePropertySchema.bind(this),
6376
- resolveDiscriminatorMapping: this.resolveDiscriminatorMapping.bind(this)
6514
+ resolveDiscriminatorMapping: this.resolveDiscriminatorMapping.bind(this),
6515
+ resolveSchemaRef: this.resolveSchemaRef.bind(this)
6377
6516
  },
6378
6517
  {
6379
6518
  passthrough: needsPassthrough,
@@ -6394,7 +6533,8 @@ var _PropertyGenerator = class _PropertyGenerator {
6394
6533
  nullable,
6395
6534
  {
6396
6535
  generatePropertySchema: this.generatePropertySchema.bind(this),
6397
- resolveDiscriminatorMapping: this.resolveDiscriminatorMapping.bind(this)
6536
+ resolveDiscriminatorMapping: this.resolveDiscriminatorMapping.bind(this),
6537
+ resolveSchemaRef: this.resolveSchemaRef.bind(this)
6398
6538
  },
6399
6539
  {
6400
6540
  passthrough: needsPassthrough,
@@ -6457,7 +6597,17 @@ var _PropertyGenerator = class _PropertyGenerator {
6457
6597
  );
6458
6598
  validation = addDescription(validation, schema.description, this.context.useDescribe);
6459
6599
  } else {
6460
- validation = "z.record(z.string(), z.unknown())";
6600
+ switch (this.context.emptyObjectBehavior) {
6601
+ case "strict":
6602
+ validation = "z.strictObject({})";
6603
+ break;
6604
+ case "loose":
6605
+ validation = "z.looseObject({})";
6606
+ break;
6607
+ default:
6608
+ validation = "z.record(z.string(), z.unknown())";
6609
+ break;
6610
+ }
6461
6611
  validation = addDescription(validation, schema.description, this.context.useDescribe);
6462
6612
  }
6463
6613
  break;
@@ -6472,6 +6622,44 @@ var _PropertyGenerator = class _PropertyGenerator {
6472
6622
  }
6473
6623
  return result;
6474
6624
  }
6625
+ /**
6626
+ * Generate inline object shape for use with .extend()
6627
+ * Returns just the shape object literal: { prop1: z.string(), prop2: z.number() }
6628
+ *
6629
+ * This method is specifically for allOf compositions where we need to pass
6630
+ * the shape directly to .extend() instead of using z.object({...}).shape.
6631
+ * This avoids the .nullable().shape bug when inline objects have nullable: true.
6632
+ *
6633
+ * According to Zod docs (https://zod.dev/api?id=extend):
6634
+ * - .extend() accepts an object of shape definitions
6635
+ * - e.g., baseSchema.extend({ prop: z.string() })
6636
+ */
6637
+ generateInlineObjectShape(schema, currentSchema) {
6638
+ const required = new Set(schema.required || []);
6639
+ const properties = [];
6640
+ if (schema.properties) {
6641
+ for (const [propName, propSchema] of Object.entries(schema.properties)) {
6642
+ if (!this.shouldIncludeProperty(propSchema)) {
6643
+ continue;
6644
+ }
6645
+ const isRequired = required.has(propName);
6646
+ const zodSchema = this.generatePropertySchema(propSchema, currentSchema);
6647
+ const validIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
6648
+ const quotedPropName = validIdentifier.test(propName) ? propName : `"${propName}"`;
6649
+ let propertyDef = `${quotedPropName}: ${zodSchema}`;
6650
+ if (!isRequired) {
6651
+ propertyDef += ".optional()";
6652
+ }
6653
+ properties.push(propertyDef);
6654
+ }
6655
+ }
6656
+ if (properties.length === 0) {
6657
+ return "{}";
6658
+ }
6659
+ return `{
6660
+ ${properties.map((p) => ` ${p}`).join(",\n")}
6661
+ }`;
6662
+ }
6475
6663
  };
6476
6664
  // Performance optimization: Lookup table for faster inclusion checks
6477
6665
  _PropertyGenerator.INCLUSION_RULES = {
@@ -6616,6 +6804,60 @@ function formatFilterStatistics(stats) {
6616
6804
  return lines.join("\n");
6617
6805
  }
6618
6806
 
6807
+ // src/utils/ref-resolver.ts
6808
+ init_cjs_shims();
6809
+ function resolveRef2(obj, spec, maxDepth = 10) {
6810
+ var _a, _b, _c, _d;
6811
+ if (!obj || typeof obj !== "object" || maxDepth <= 0) return obj;
6812
+ if (!obj.$ref) return obj;
6813
+ const ref = obj.$ref;
6814
+ let resolved = null;
6815
+ const paramMatch = ref.match(/^#\/components\/parameters\/(.+)$/);
6816
+ const requestBodyMatch = ref.match(/^#\/components\/requestBodies\/(.+)$/);
6817
+ const responseMatch = ref.match(/^#\/components\/responses\/(.+)$/);
6818
+ const schemaMatch = ref.match(/^#\/components\/schemas\/(.+)$/);
6819
+ if (paramMatch && ((_a = spec.components) == null ? void 0 : _a.parameters)) {
6820
+ const name = paramMatch[1];
6821
+ resolved = spec.components.parameters[name];
6822
+ } else if (requestBodyMatch && ((_b = spec.components) == null ? void 0 : _b.requestBodies)) {
6823
+ const name = requestBodyMatch[1];
6824
+ resolved = spec.components.requestBodies[name];
6825
+ } else if (responseMatch && ((_c = spec.components) == null ? void 0 : _c.responses)) {
6826
+ const name = responseMatch[1];
6827
+ resolved = spec.components.responses[name];
6828
+ } else if (schemaMatch && ((_d = spec.components) == null ? void 0 : _d.schemas)) {
6829
+ const name = schemaMatch[1];
6830
+ resolved = spec.components.schemas[name];
6831
+ }
6832
+ if (resolved) {
6833
+ if (resolved.$ref) {
6834
+ return resolveRef2(resolved, spec, maxDepth - 1);
6835
+ }
6836
+ return resolved;
6837
+ }
6838
+ return obj;
6839
+ }
6840
+ function resolveParameterRef(param, spec) {
6841
+ return resolveRef2(param, spec);
6842
+ }
6843
+ function mergeParameters(pathParams, operationParams, spec) {
6844
+ const resolvedPathParams = (pathParams || []).map((p) => resolveParameterRef(p, spec));
6845
+ const resolvedOperationParams = (operationParams || []).map((p) => resolveParameterRef(p, spec));
6846
+ const merged = [...resolvedPathParams];
6847
+ for (const opParam of resolvedOperationParams) {
6848
+ if (!opParam || typeof opParam !== "object") continue;
6849
+ const existingIndex = merged.findIndex(
6850
+ (p) => p && typeof p === "object" && p.name === opParam.name && p.in === opParam.in
6851
+ );
6852
+ if (existingIndex >= 0) {
6853
+ merged[existingIndex] = opParam;
6854
+ } else {
6855
+ merged.push(opParam);
6856
+ }
6857
+ }
6858
+ return merged;
6859
+ }
6860
+
6619
6861
  // src/openapi-generator.ts
6620
6862
  var OpenApiGenerator = class {
6621
6863
  constructor(options) {
@@ -6625,7 +6867,7 @@ var OpenApiGenerator = class {
6625
6867
  this.schemaUsageMap = /* @__PURE__ */ new Map();
6626
6868
  this.needsZodImport = true;
6627
6869
  this.filterStats = createFilterStatistics();
6628
- var _a, _b, _c, _d, _e, _f, _g;
6870
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
6629
6871
  if (!options.input) {
6630
6872
  throw new ConfigurationError("Input path is required", { providedOptions: options });
6631
6873
  }
@@ -6636,18 +6878,19 @@ var OpenApiGenerator = class {
6636
6878
  includeDescriptions: (_a = options.includeDescriptions) != null ? _a : true,
6637
6879
  useDescribe: (_b = options.useDescribe) != null ? _b : false,
6638
6880
  defaultNullable: (_c = options.defaultNullable) != null ? _c : false,
6881
+ emptyObjectBehavior: (_d = options.emptyObjectBehavior) != null ? _d : "loose",
6639
6882
  schemaType: options.schemaType || "all",
6640
6883
  prefix: options.prefix,
6641
6884
  suffix: options.suffix,
6642
6885
  stripSchemaPrefix: options.stripSchemaPrefix,
6643
6886
  stripPathPrefix: options.stripPathPrefix,
6644
- showStats: (_d = options.showStats) != null ? _d : true,
6887
+ showStats: (_e = options.showStats) != null ? _e : true,
6645
6888
  request: options.request,
6646
6889
  response: options.response,
6647
6890
  operationFilters: options.operationFilters,
6648
6891
  ignoreHeaders: options.ignoreHeaders,
6649
- cacheSize: (_e = options.cacheSize) != null ? _e : 1e3,
6650
- batchSize: (_f = options.batchSize) != null ? _f : 10,
6892
+ cacheSize: (_f = options.cacheSize) != null ? _f : 1e3,
6893
+ batchSize: (_g = options.batchSize) != null ? _g : 10,
6651
6894
  customDateTimeFormatRegex: options.customDateTimeFormatRegex
6652
6895
  };
6653
6896
  if (this.options.cacheSize) {
@@ -6718,7 +6961,8 @@ var OpenApiGenerator = class {
6718
6961
  mode: this.requestOptions.mode,
6719
6962
  includeDescriptions: this.requestOptions.includeDescriptions,
6720
6963
  useDescribe: this.requestOptions.useDescribe,
6721
- defaultNullable: (_g = this.options.defaultNullable) != null ? _g : false,
6964
+ defaultNullable: (_h = this.options.defaultNullable) != null ? _h : false,
6965
+ emptyObjectBehavior: (_i = this.options.emptyObjectBehavior) != null ? _i : "loose",
6722
6966
  namingOptions: {
6723
6967
  prefix: this.options.prefix,
6724
6968
  suffix: this.options.suffix
@@ -7072,7 +7316,7 @@ var OpenApiGenerator = class {
7072
7316
  * Generate schema for a component
7073
7317
  */
7074
7318
  generateComponentSchema(name, schema) {
7075
- var _a, _b, _c;
7319
+ var _a, _b, _c, _d;
7076
7320
  if (!this.schemaDependencies.has(name)) {
7077
7321
  this.schemaDependencies.set(name, /* @__PURE__ */ new Set());
7078
7322
  }
@@ -7105,6 +7349,7 @@ ${typeCode}`;
7105
7349
  includeDescriptions: resolvedOptions.includeDescriptions,
7106
7350
  useDescribe: resolvedOptions.useDescribe,
7107
7351
  defaultNullable: (_b = this.options.defaultNullable) != null ? _b : false,
7352
+ emptyObjectBehavior: (_c = this.options.emptyObjectBehavior) != null ? _c : "loose",
7108
7353
  namingOptions: {
7109
7354
  prefix: this.options.prefix,
7110
7355
  suffix: this.options.suffix
@@ -7121,7 +7366,7 @@ ${typeCode}`;
7121
7366
  const depMatch = ref.match(/^([a-z][a-zA-Z0-9]*?)Schema$/);
7122
7367
  if (depMatch) {
7123
7368
  const depName = depMatch[1].charAt(0).toUpperCase() + depMatch[1].slice(1);
7124
- (_c = this.schemaDependencies.get(name)) == null ? void 0 : _c.add(depName);
7369
+ (_d = this.schemaDependencies.get(name)) == null ? void 0 : _d.add(depName);
7125
7370
  }
7126
7371
  }
7127
7372
  }
@@ -7145,10 +7390,8 @@ ${typeCode}`;
7145
7390
  if (!shouldIncludeOperation(operation, path, method, this.options.operationFilters)) {
7146
7391
  continue;
7147
7392
  }
7148
- if (!operation.parameters || !Array.isArray(operation.parameters)) {
7149
- continue;
7150
- }
7151
- const queryParams = operation.parameters.filter(
7393
+ const allParams = mergeParameters(pathItem.parameters, operation.parameters, this.spec);
7394
+ const queryParams = allParams.filter(
7152
7395
  (param) => param && typeof param === "object" && param.in === "query"
7153
7396
  );
7154
7397
  if (queryParams.length === 0) {
@@ -7284,10 +7527,8 @@ ${propsCode}
7284
7527
  if (!shouldIncludeOperation(operation, path, method, this.options.operationFilters)) {
7285
7528
  continue;
7286
7529
  }
7287
- if (!operation.parameters || !Array.isArray(operation.parameters)) {
7288
- continue;
7289
- }
7290
- const headerParams = operation.parameters.filter(
7530
+ const allParams = mergeParameters(pathItem.parameters, operation.parameters, this.spec);
7531
+ const headerParams = allParams.filter(
7291
7532
  (param) => param && typeof param === "object" && param.in === "header" && !this.shouldIgnoreHeader(param.name)
7292
7533
  );
7293
7534
  if (headerParams.length === 0) {
@@ -7377,13 +7618,23 @@ ${propsCode}
7377
7618
  }
7378
7619
  const type = schema.type;
7379
7620
  if (type === "string") {
7621
+ const formatMap = {
7622
+ email: "z.email()",
7623
+ uri: "z.url()",
7624
+ url: "z.url()",
7625
+ uuid: "z.uuid()"
7626
+ };
7627
+ if (schema.format && formatMap[schema.format]) {
7628
+ let zodType2 = formatMap[schema.format];
7629
+ if (schema.minLength !== void 0) zodType2 = `${zodType2}.min(${schema.minLength})`;
7630
+ if (schema.maxLength !== void 0) zodType2 = `${zodType2}.max(${schema.maxLength})`;
7631
+ if (schema.pattern) zodType2 = `${zodType2}.regex(/${schema.pattern}/)`;
7632
+ return zodType2;
7633
+ }
7380
7634
  let zodType = "z.string()";
7381
7635
  if (schema.minLength !== void 0) zodType = `${zodType}.min(${schema.minLength})`;
7382
7636
  if (schema.maxLength !== void 0) zodType = `${zodType}.max(${schema.maxLength})`;
7383
7637
  if (schema.pattern) zodType = `${zodType}.regex(/${schema.pattern}/)`;
7384
- if (schema.format === "email") zodType = `${zodType}.email()`;
7385
- if (schema.format === "uri" || schema.format === "url") zodType = `${zodType}.url()`;
7386
- if (schema.format === "uuid") zodType = `${zodType}.uuid()`;
7387
7638
  return zodType;
7388
7639
  }
7389
7640
  if (type === "number" || type === "integer") {
@@ -7408,11 +7659,6 @@ ${propsCode}
7408
7659
  }
7409
7660
  return "z.unknown()";
7410
7661
  }
7411
- // REMOVED: generateNativeEnum method - no longer needed as we only generate Zod schemas
7412
- // REMOVED: toEnumKey method - was only used by generateNativeEnum
7413
- // REMOVED: addConstraintsToJSDoc method - was only used for native TypeScript types
7414
- // REMOVED: generateNativeTypeDefinition method - was only used for native TypeScript types
7415
- // REMOVED: generateObjectType method - was only used for native TypeScript types
7416
7662
  /**
7417
7663
  * Topological sort for schema dependencies
7418
7664
  * Returns schemas in the order they should be declared