@cerios/openapi-to-zod 1.5.0 → 1.6.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
@@ -188,6 +188,7 @@ Examples:
188
188
  | `name` | `string` | Optional identifier for logging |
189
189
  | `input` | `string` | Input OpenAPI YAML file path (required) |
190
190
  | `outputTypes` | `string` | Preferred output TypeScript file path (required unless deprecated `output` is set) |
191
+ | `outputZodSchemas` | `string` | Separate output path for Zod schemas (recommended for circular references, see below) |
191
192
  | `output` | `string` | Deprecated alias for `outputTypes`; allowed for backward compatibility |
192
193
  | `mode` | `"strict"` \| `"normal"` \| `"loose"` | Validation mode for top-level schemas (default: `"normal"`) |
193
194
  | `emptyObjectBehavior` | `"strict"` \| `"loose"` \| `"record"` | How to handle empty objects (default: `"loose"`) |
@@ -1087,6 +1088,54 @@ export default defineConfig({
1087
1088
  4. **Better Code Completion**: Easier to find schemas in IDE autocomplete
1088
1089
  5. **Flexible Pattern Matching**: Use regex for dynamic prefixes
1089
1090
 
1091
+ ## Circular References and `z.lazy()`
1092
+
1093
+ 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:
1094
+
1095
+ ```typescript
1096
+ // Combined mode - can cause TypeScript errors
1097
+ export const nodeSchema = z.object({
1098
+ id: z.string(),
1099
+ parent: z.lazy(() => nodeSchema).optional(), // ❌ Type errors with circular inference
1100
+ });
1101
+ export type Node = z.infer<typeof nodeSchema>; // Circular type reference
1102
+ ```
1103
+
1104
+ **Recommendation:** Use separate type and schema files (`outputZodSchemas`) for specs with circular references:
1105
+
1106
+ ```typescript
1107
+ import { defineConfig } from "@cerios/openapi-to-zod";
1108
+
1109
+ export default defineConfig({
1110
+ specs: [
1111
+ {
1112
+ input: "openapi.yaml",
1113
+ outputTypes: "src/generated/types.ts", // TypeScript types
1114
+ outputZodSchemas: "src/generated/schemas.ts", // Zod schemas
1115
+ },
1116
+ ],
1117
+ });
1118
+ ```
1119
+
1120
+ This generates proper forward-declared types:
1121
+
1122
+ ```typescript
1123
+ // types.ts
1124
+ export interface Node {
1125
+ id?: string;
1126
+ parent?: Node;
1127
+ }
1128
+
1129
+ // schemas.ts
1130
+ import type { Node } from "./types";
1131
+ export const nodeSchema: z.ZodType<Node> = z.object({
1132
+ id: z.string().optional(),
1133
+ parent: z.lazy(() => nodeSchema).optional(),
1134
+ });
1135
+ ```
1136
+
1137
+ This approach also helps avoid "Type instantiation is excessively deep" errors (TS2589) with large schemas.
1138
+
1090
1139
  ## Generation Statistics
1091
1140
 
1092
1141
  Statistics are **included by default** in generated files. Use `showStats: false` to disable:
package/dist/cli.js CHANGED
@@ -5132,9 +5132,11 @@ function isDiscriminatorRequired(schemas, discriminator, context) {
5132
5132
  };
5133
5133
  }
5134
5134
  function generateUnion(schemas, discriminator, isNullable2, context, options, currentSchema) {
5135
+ var _a, _b;
5135
5136
  if (schemas.length === 0) {
5136
- console.warn(
5137
- "[openapi-to-zod] Warning: Empty oneOf/anyOf array encountered. This is likely a malformed OpenAPI spec. Generating z.never() as fallback."
5137
+ (_a = context.warn) == null ? void 0 : _a.call(
5138
+ context,
5139
+ "Empty oneOf/anyOf array encountered. This is likely a malformed OpenAPI spec. Generating z.never() as fallback."
5138
5140
  );
5139
5141
  return wrapNullable(
5140
5142
  'z.never().describe("Empty oneOf/anyOf in OpenAPI spec - no valid schema defined")',
@@ -5155,8 +5157,9 @@ function generateUnion(schemas, discriminator, isNullable2, context, options, cu
5155
5157
  }
5156
5158
  const discriminatorCheck = isDiscriminatorRequired(resolvedSchemas, discriminator, context);
5157
5159
  if (!discriminatorCheck.valid) {
5158
- console.warn(
5159
- `[openapi-to-zod] Warning: Discriminator "${discriminator}" is not required in schemas: ${discriminatorCheck.invalidSchemas.join(", ")}. Falling back to z.union() instead of z.discriminatedUnion().`
5160
+ (_b = context.warn) == null ? void 0 : _b.call(
5161
+ context,
5162
+ `Discriminator "${discriminator}" is not required in schemas: ${discriminatorCheck.invalidSchemas.join(", ")}. Falling back to z.union() instead of z.discriminatedUnion().`
5160
5163
  );
5161
5164
  let schemaStrings3 = resolvedSchemas.map((s) => context.generatePropertySchema(s, currentSchema, false, true));
5162
5165
  if (options == null ? void 0 : options.passthrough) {
@@ -5234,6 +5237,7 @@ function detectConflictingProperties(schemas, context) {
5234
5237
  return conflicts;
5235
5238
  }
5236
5239
  function generateAllOf(schemas, isNullable2, context, currentSchema) {
5240
+ var _a;
5237
5241
  if (schemas.length === 1) {
5238
5242
  const singleSchema = context.generatePropertySchema(schemas[0], currentSchema, false, true);
5239
5243
  return { schema: wrapNullable(singleSchema, isNullable2), conflicts: [] };
@@ -5242,7 +5246,7 @@ function generateAllOf(schemas, isNullable2, context, currentSchema) {
5242
5246
  const uniqueConflicts = [...new Set(conflicts)];
5243
5247
  if (uniqueConflicts.length > 0) {
5244
5248
  for (const conflict of uniqueConflicts) {
5245
- console.warn(`[openapi-to-zod] Warning: allOf composition conflict - ${conflict}`);
5249
+ (_a = context.warn) == null ? void 0 : _a.call(context, `allOf composition conflict - ${conflict}`);
5246
5250
  }
5247
5251
  }
5248
5252
  const allObjects = schemas.every((s) => s.type === "object" || s.properties || s.$ref || s.allOf);
@@ -6163,7 +6167,8 @@ var _PropertyGenerator = class _PropertyGenerator {
6163
6167
  {
6164
6168
  generatePropertySchema: this.generatePropertySchema.bind(this),
6165
6169
  generateInlineObjectShape: this.generateInlineObjectShape.bind(this),
6166
- resolveSchemaRef: this.resolveSchemaRef.bind(this)
6170
+ resolveSchemaRef: this.resolveSchemaRef.bind(this),
6171
+ warn: this.context.warn
6167
6172
  },
6168
6173
  currentSchema
6169
6174
  );
@@ -6186,7 +6191,8 @@ var _PropertyGenerator = class _PropertyGenerator {
6186
6191
  {
6187
6192
  generatePropertySchema: this.generatePropertySchema.bind(this),
6188
6193
  resolveDiscriminatorMapping: this.resolveDiscriminatorMapping.bind(this),
6189
- resolveSchemaRef: this.resolveSchemaRef.bind(this)
6194
+ resolveSchemaRef: this.resolveSchemaRef.bind(this),
6195
+ warn: this.context.warn
6190
6196
  },
6191
6197
  {
6192
6198
  passthrough: needsPassthrough,
@@ -6209,7 +6215,8 @@ var _PropertyGenerator = class _PropertyGenerator {
6209
6215
  {
6210
6216
  generatePropertySchema: this.generatePropertySchema.bind(this),
6211
6217
  resolveDiscriminatorMapping: this.resolveDiscriminatorMapping.bind(this),
6212
- resolveSchemaRef: this.resolveSchemaRef.bind(this)
6218
+ resolveSchemaRef: this.resolveSchemaRef.bind(this),
6219
+ warn: this.context.warn
6213
6220
  },
6214
6221
  {
6215
6222
  passthrough: needsPassthrough,
@@ -6364,8 +6371,6 @@ var OpenApiGenerator = class {
6364
6371
  this.schemaUsageMap = /* @__PURE__ */ new Map();
6365
6372
  this.needsZodImport = true;
6366
6373
  this.filterStats = (0, import_openapi_core6.createFilterStatistics)();
6367
- /** Track total allOf conflicts detected across all schemas */
6368
- this.allOfConflictCount = 0;
6369
6374
  /** Track schemas involved in circular dependency chains */
6370
6375
  this.circularDependencies = /* @__PURE__ */ new Set();
6371
6376
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
@@ -6373,6 +6378,11 @@ var OpenApiGenerator = class {
6373
6378
  throw new import_openapi_core6.ConfigurationError("Input path is required", { providedOptions: options });
6374
6379
  }
6375
6380
  this.separateSchemasMode = Boolean(options.outputZodSchemas);
6381
+ const showWarnings = options.showWarnings !== false;
6382
+ this.warningCollector = new import_openapi_core6.WarningCollector({
6383
+ packageName: "@cerios/openapi-to-zod",
6384
+ enabled: showWarnings
6385
+ });
6376
6386
  this.options = {
6377
6387
  mode: options.mode || "normal",
6378
6388
  input: options.input,
@@ -6391,13 +6401,16 @@ var OpenApiGenerator = class {
6391
6401
  stripPathPrefix: options.stripPathPrefix,
6392
6402
  useOperationId: (_f = options.useOperationId) != null ? _f : true,
6393
6403
  showStats: (_g = options.showStats) != null ? _g : true,
6404
+ showWarnings,
6394
6405
  request: options.request,
6395
6406
  response: options.response,
6396
6407
  operationFilters: options.operationFilters,
6397
6408
  ignoreHeaders: options.ignoreHeaders,
6398
6409
  cacheSize: (_h = options.cacheSize) != null ? _h : 1e3,
6399
6410
  batchSize: (_i = options.batchSize) != null ? _i : 10,
6400
- customDateTimeFormatRegex: options.customDateTimeFormatRegex
6411
+ customDateTimeFormatRegex: options.customDateTimeFormatRegex,
6412
+ includeHeader: options.includeHeader,
6413
+ fileHeader: options.fileHeader
6401
6414
  };
6402
6415
  this.patternCache = new import_openapi_core6.LRUCache((_j = this.options.cacheSize) != null ? _j : 1e3);
6403
6416
  this.dateTimeValidation = buildDateTimeValidation(this.options.customDateTimeFormatRegex);
@@ -6422,7 +6435,10 @@ var OpenApiGenerator = class {
6422
6435
  stripSchemaPrefix: this.options.stripSchemaPrefix,
6423
6436
  dateTimeValidation: this.dateTimeValidation,
6424
6437
  patternCache: this.patternCache,
6425
- separateTypesFile: this.separateSchemasMode
6438
+ separateTypesFile: this.separateSchemasMode,
6439
+ warn: (msg) => {
6440
+ this.warningCollector.add(msg);
6441
+ }
6426
6442
  });
6427
6443
  }
6428
6444
  /**
@@ -6431,7 +6447,7 @@ var OpenApiGenerator = class {
6431
6447
  * @returns The generated TypeScript code as a string
6432
6448
  */
6433
6449
  generateString() {
6434
- var _a;
6450
+ var _a, _b, _c;
6435
6451
  if (!((_a = this.spec.components) == null ? void 0 : _a.schemas)) {
6436
6452
  throw new import_openapi_core6.SpecValidationError("No schemas found in OpenAPI spec", { filePath: this.options.input });
6437
6453
  }
@@ -6448,9 +6464,26 @@ var OpenApiGenerator = class {
6448
6464
  }
6449
6465
  this.generateQueryParameterSchemas();
6450
6466
  this.generateHeaderParameterSchemas();
6451
- (0, import_openapi_core6.validateFilters)(this.filterStats, this.options.operationFilters);
6467
+ (0, import_openapi_core6.validateFilters)(this.filterStats, this.options.operationFilters, (msg) => {
6468
+ this.warningCollector.add(msg);
6469
+ });
6452
6470
  const orderedSchemaNames = this.topologicalSort();
6453
- const output = ["// Auto-generated by @cerios/openapi-to-zod", "// Do not edit this file manually", ""];
6471
+ const output = [];
6472
+ const customHeader = (0, import_openapi_core6.generateCustomFileHeader)(this.options.fileHeader);
6473
+ if (customHeader) {
6474
+ output.push(customHeader.trimEnd());
6475
+ output.push("");
6476
+ }
6477
+ if (this.options.includeHeader !== false) {
6478
+ output.push(
6479
+ (0, import_openapi_core6.generateFileHeader)({
6480
+ packageName: "@cerios/openapi-to-zod",
6481
+ apiTitle: (_b = this.spec.info) == null ? void 0 : _b.title,
6482
+ apiVersion: (_c = this.spec.info) == null ? void 0 : _c.version
6483
+ }).trimEnd()
6484
+ );
6485
+ output.push("");
6486
+ }
6454
6487
  if (this.options.showStats === true) {
6455
6488
  output.push(...this.generateStats());
6456
6489
  output.push("");
@@ -6477,6 +6510,7 @@ var OpenApiGenerator = class {
6477
6510
  output.push("");
6478
6511
  }
6479
6512
  }
6513
+ this.warningCollector.flush();
6480
6514
  return output.join("\n");
6481
6515
  }
6482
6516
  /**
@@ -6516,6 +6550,7 @@ var OpenApiGenerator = class {
6516
6550
  (0, import_node_fs.writeFileSync)(normalizedOutput, output);
6517
6551
  console.log(` \u2713 Generated ${normalizedOutput}`);
6518
6552
  }
6553
+ this.warningCollector.flush();
6519
6554
  }
6520
6555
  /**
6521
6556
  * Generate Zod schemas with explicit type annotations (for outputZodSchemas mode)
@@ -6523,7 +6558,7 @@ var OpenApiGenerator = class {
6523
6558
  * @returns The generated Zod schemas TypeScript code
6524
6559
  */
6525
6560
  generateSeparateSchemasString() {
6526
- var _a;
6561
+ var _a, _b, _c;
6527
6562
  const schemas = (_a = this.spec.components) == null ? void 0 : _a.schemas;
6528
6563
  if (!schemas) {
6529
6564
  return "";
@@ -6542,9 +6577,24 @@ var OpenApiGenerator = class {
6542
6577
  }
6543
6578
  this.generateQueryParameterSchemas();
6544
6579
  this.generateHeaderParameterSchemas();
6545
- (0, import_openapi_core6.validateFilters)(this.filterStats, this.options.operationFilters);
6580
+ (0, import_openapi_core6.validateFilters)(this.filterStats, this.options.operationFilters, (msg) => {
6581
+ this.warningCollector.add(msg);
6582
+ });
6546
6583
  const orderedSchemaNames = this.topologicalSort();
6547
- const output = ["// Auto-generated by @cerios/openapi-to-zod", "// Do not edit this file manually", ""];
6584
+ const output = [];
6585
+ const customHeader = (0, import_openapi_core6.generateCustomFileHeader)(this.options.fileHeader);
6586
+ if (customHeader) {
6587
+ output.push(customHeader.trimEnd());
6588
+ output.push("");
6589
+ }
6590
+ output.push(
6591
+ (0, import_openapi_core6.generateFileHeader)({
6592
+ packageName: "@cerios/openapi-to-zod",
6593
+ apiTitle: (_b = this.spec.info) == null ? void 0 : _b.title,
6594
+ apiVersion: (_c = this.spec.info) == null ? void 0 : _c.version
6595
+ }).trimEnd()
6596
+ );
6597
+ output.push("");
6548
6598
  if (this.options.showStats === true) {
6549
6599
  output.push(...this.generateStats());
6550
6600
  output.push("");
@@ -6582,8 +6632,8 @@ var OpenApiGenerator = class {
6582
6632
  * @returns The generated TypeScript types code
6583
6633
  */
6584
6634
  generateTypesString() {
6585
- var _a;
6586
- const tsGenerator = new import_openapi_to_typescript.TypeScriptGenerator({
6635
+ var _a, _b, _c;
6636
+ const internalOptions = {
6587
6637
  input: this.options.input,
6588
6638
  outputTypes: this.options.outputTypes,
6589
6639
  includeDescriptions: this.options.includeDescriptions,
@@ -6594,9 +6644,20 @@ var OpenApiGenerator = class {
6594
6644
  stripPathPrefix: this.options.stripPathPrefix,
6595
6645
  operationFilters: this.options.operationFilters,
6596
6646
  showStats: this.options.showStats,
6597
- enumFormat: (_a = this.options.enumFormat) != null ? _a : "const-object"
6647
+ enumFormat: (_a = this.options.enumFormat) != null ? _a : "const-object",
6648
+ includeHeader: false,
6649
+ // We add our own header for consistent branding
6650
+ showWarnings: false
6651
+ // We handle warnings ourselves
6652
+ };
6653
+ const tsGenerator = new import_openapi_to_typescript.TypeScriptGenerator(internalOptions);
6654
+ const customHeader = (0, import_openapi_core6.generateCustomFileHeader)(this.options.fileHeader);
6655
+ const header = (0, import_openapi_core6.generateFileHeader)({
6656
+ packageName: "@cerios/openapi-to-zod",
6657
+ apiTitle: (_b = this.spec.info) == null ? void 0 : _b.title,
6658
+ apiVersion: (_c = this.spec.info) == null ? void 0 : _c.version
6598
6659
  });
6599
- return tsGenerator.generateString();
6660
+ return customHeader + header + tsGenerator.generateString();
6600
6661
  }
6601
6662
  /**
6602
6663
  * Add explicit type annotation to a schema declaration
@@ -6915,14 +6976,16 @@ ${typeCode}`;
6915
6976
  stripSchemaPrefix: this.options.stripSchemaPrefix,
6916
6977
  dateTimeValidation: this.dateTimeValidation,
6917
6978
  patternCache: this.patternCache,
6918
- separateTypesFile: this.separateSchemasMode
6979
+ separateTypesFile: this.separateSchemasMode,
6980
+ warn: (msg) => {
6981
+ this.warningCollector.add(msg);
6982
+ }
6919
6983
  });
6920
6984
  this.propertyGenerator.setCircularDependencies(this.circularDependencies);
6921
6985
  this.propertyGenerator.clearAllOfConflicts();
6922
6986
  const zodSchema = this.propertyGenerator.generatePropertySchema(schema, name, true);
6923
6987
  const allOfConflicts = this.propertyGenerator.getAllOfConflicts();
6924
6988
  if (allOfConflicts.length > 0) {
6925
- this.allOfConflictCount += allOfConflicts.length;
6926
6989
  const conflictWarning = this.generateConflictJSDoc(allOfConflicts);
6927
6990
  if (jsdoc) {
6928
6991
  jsdoc = jsdoc.replace(/ \*\/\n$/, `
@@ -7286,8 +7349,7 @@ ${propsCode}
7286
7349
  `// Total schemas: ${stats.totalSchemas}`,
7287
7350
  `// Circular references: ${stats.withCircularRefs}`,
7288
7351
  `// Discriminated unions: ${stats.withDiscriminators}`,
7289
- `// With constraints: ${stats.withConstraints}`,
7290
- `// AllOf conflicts: ${this.allOfConflictCount}`
7352
+ `// With constraints: ${stats.withConstraints}`
7291
7353
  ];
7292
7354
  if (this.options.operationFilters && this.filterStats.totalOperations > 0) {
7293
7355
  output.push("//");
@@ -7519,6 +7581,7 @@ function mergeConfigWithDefaults(config) {
7519
7581
  showStats: defaults.showStats,
7520
7582
  customDateTimeFormatRegex: defaults.customDateTimeFormatRegex,
7521
7583
  enumFormat: defaults.enumFormat,
7584
+ fileHeader: defaults.fileHeader,
7522
7585
  // Override with spec-specific values
7523
7586
  ...specWithoutDeprecatedOutput,
7524
7587
  outputTypes: resolvedOutputTypes