@kubb/plugin-zod 5.0.0-beta.76 → 5.0.0-beta.79

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/dist/index.d.ts CHANGED
@@ -119,6 +119,12 @@ type PrinterZodOptions = {
119
119
  * variant for each date-bearing component.
120
120
  */
121
121
  direction?: 'input' | 'output';
122
+ /**
123
+ * Maps a component `$ref` path to its collision-resolved name. When two components collide
124
+ * (across sections or by case), the adapter renames one of them; the `ref()` handler resolves
125
+ * the referenced name through this map so the emitted schema reference matches the renamed component.
126
+ */
127
+ nameMapping?: ReadonlyMap<string, string>;
122
128
  /**
123
129
  * Custom handler map for node type overrides.
124
130
  */
@@ -196,6 +202,12 @@ type PrinterZodMiniOptions = {
196
202
  * Properties referencing these emit lazy getters wrapping refs in `z.lazy(() => …)`.
197
203
  */
198
204
  cyclicSchemas?: ReadonlySet<string>;
205
+ /**
206
+ * Maps a component `$ref` path to its collision-resolved name. When two components collide
207
+ * (across sections or by case), the adapter renames one of them; the `ref()` handler resolves
208
+ * the referenced name through this map so the emitted schema reference matches the renamed component.
209
+ */
210
+ nameMapping?: ReadonlyMap<string, string>;
199
211
  /**
200
212
  * Custom handler map for node type overrides.
201
213
  */
@@ -457,7 +469,7 @@ declare const pluginZodName = "plugin-zod";
457
469
  * Generates Zod v4 schemas from an OpenAPI spec. Use them to validate API
458
470
  * responses at runtime, build form schemas, or feed back into router libraries
459
471
  * that consume Zod (tRPC, Hono, Elysia). Pair with `@kubb/plugin-axios` or
460
- * `@kubb/plugin-fetch` and set the client's `parser: 'zod'` to validate every response
472
+ * `@kubb/plugin-fetch` and set the client's `validator: 'zod'` to validate every response
461
473
  * automatically.
462
474
  *
463
475
  * @example
package/dist/index.js CHANGED
@@ -72,7 +72,7 @@ function toFilePath(name, caseLast = camelCase) {
72
72
  * JavaScript and Java reserved words.
73
73
  * @link https://github.com/jonschlinkert/reserved/blob/master/index.js
74
74
  */
75
- const reservedWords = new Set([
75
+ const reservedWords = /* @__PURE__ */ new Set([
76
76
  "abstract",
77
77
  "arguments",
78
78
  "boolean",
@@ -304,9 +304,10 @@ function createGroupConfig(group) {
304
304
  }
305
305
  //#endregion
306
306
  //#region src/components/Zod.tsx
307
- function Zod({ name, node, printer, inferTypeName }) {
307
+ function Zod({ name, node, printer, inferTypeName, cyclic }) {
308
308
  const output = printer.print(node);
309
309
  if (!output) return;
310
+ const needsAnnotation = cyclic && node.type !== "object";
310
311
  return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(File.Source, {
311
312
  name,
312
313
  isExportable: true,
@@ -314,6 +315,7 @@ function Zod({ name, node, printer, inferTypeName }) {
314
315
  children: /* @__PURE__ */ jsx(Const, {
315
316
  export: true,
316
317
  name,
318
+ type: needsAnnotation ? "z.ZodType" : void 0,
317
319
  children: output
318
320
  })
319
321
  }), inferTypeName && /* @__PURE__ */ jsx(File.Source, {
@@ -334,7 +336,7 @@ function Zod({ name, node, printer, inferTypeName }) {
334
336
  * Import paths that use a namespace import (`import * as z from '...'`).
335
337
  * All other import paths use a named import (`import { z } from '...'`).
336
338
  */
337
- const ZOD_NAMESPACE_IMPORTS = new Set(["zod", "zod/mini"]);
339
+ const ZOD_NAMESPACE_IMPORTS = /* @__PURE__ */ new Set(["zod", "zod/mini"]);
338
340
  //#endregion
339
341
  //#region src/utils.ts
340
342
  /**
@@ -417,6 +419,33 @@ function formatDefault(value) {
417
419
  return String(value ?? "");
418
420
  }
419
421
  /**
422
+ * Format a default for `.default(...)` so the literal matches the generated schema's type.
423
+ *
424
+ * An OpenAPI `default` does not always agree with the schema it sits on: a `bigint` field
425
+ * (`format: int64`) carries a `number` default, and a spec may put `default: {}` on an `array`.
426
+ * Emitting those verbatim produces a Zod schema that does not typecheck, so coerce the bigint case
427
+ * to a `BigInt(...)` literal and emit an array literal for arrays (dropping a non-array default).
428
+ * Returns `null` when no `.default(...)` should be emitted. Keys off the schema node's type.
429
+ */
430
+ function defaultLiteral(node, value) {
431
+ if (value === null) return null;
432
+ if (node && ast.narrowSchema(node, "bigint")) {
433
+ if (typeof value === "bigint") return `BigInt(${value})`;
434
+ if (typeof value === "number" && Number.isInteger(value)) return `BigInt(${value})`;
435
+ return null;
436
+ }
437
+ if (node && ast.narrowSchema(node, "array")) return Array.isArray(value) ? JSON.stringify(value) : null;
438
+ const enumNode = node ? ast.narrowSchema(node, "enum") : void 0;
439
+ if (enumNode) {
440
+ const values = enumNode.namedEnumValues?.map((member) => member.value) ?? enumNode.enumValues ?? [];
441
+ if (values.length) {
442
+ const match = values.find((member) => member === value || String(member) === String(value));
443
+ return match != null ? formatLiteral(match) : null;
444
+ }
445
+ }
446
+ return formatDefault(value);
447
+ }
448
+ /**
420
449
  * Format a primitive enum/literal value.
421
450
  * Strings are quoted; numbers and booleans are emitted raw.
422
451
  */
@@ -425,6 +454,18 @@ function formatLiteral(v) {
425
454
  return String(v);
426
455
  }
427
456
  /**
457
+ * Build the Zod schema for a set of literal enum values.
458
+ * `z.enum()` only accepts string members in Zod v4, so a numeric, boolean, or
459
+ * mixed set is emitted as a single `z.literal(…)` or a `z.union([z.literal(…), …])`.
460
+ * An all-string set keeps the more compact `z.enum([…])`.
461
+ */
462
+ function buildEnum(values) {
463
+ if (values.every((v) => typeof v === "string")) return `z.enum([${values.map(formatLiteral).join(", ")}])`;
464
+ const literals = values.map((v) => `z.literal(${formatLiteral(v)})`);
465
+ if (literals.length === 1) return literals[0];
466
+ return `z.union([${literals.join(", ")}])`;
467
+ }
468
+ /**
428
469
  * Map a `regexType` to the `func` argument of `toRegExpString`: `'constructor'` emits
429
470
  * `new RegExp(...)`, while `'literal'` (the default) emits a regex literal.
430
471
  */
@@ -503,36 +544,62 @@ function lengthChecksMini({ min, max, pattern, regexType }) {
503
544
  * Apply nullable / optional / nullish modifiers, an optional `.describe()` call, and an
504
545
  * optional `.meta({ examples })` call to a schema value string using the chainable Zod v4 API.
505
546
  */
506
- function applyModifiers({ value, nullable, optional, nullish, defaultValue, description, examples }) {
547
+ function applyModifiers({ value, schema, nullable, optional, nullish, defaultValue, description, examples }) {
507
548
  const withModifier = (() => {
508
549
  if (nullish || nullable && optional) return `${value}.nullish()`;
509
550
  if (optional) return `${value}.optional()`;
510
551
  if (nullable) return `${value}.nullable()`;
511
552
  return value;
512
553
  })();
513
- const withDefault = defaultValue !== void 0 ? `${withModifier}.default(${formatDefault(defaultValue)})` : withModifier;
554
+ const literal = defaultValue !== void 0 ? defaultLiteral(schema, defaultValue) : null;
555
+ const withDefault = literal !== null ? `${withModifier}.default(${literal})` : withModifier;
514
556
  const withDescription = description ? `${withDefault}.describe(${stringify(description)})` : withDefault;
515
557
  return examples?.length ? `${withDescription}.meta({ examples: [${examples.map(formatDefault).join(", ")}] })` : withDescription;
516
558
  }
559
+ function modifierDepth(schema) {
560
+ if (schema.nullish || schema.nullable && schema.optional) return 2;
561
+ if (schema.nullable || schema.optional) return 1;
562
+ return 0;
563
+ }
564
+ /**
565
+ * Build the `.unwrap()` chain to insert before `.omit()` when the schema is a `$ref`.
566
+ *
567
+ * A `$ref` resolves to a named schema variable that already carries its own `.nullable()` /
568
+ * `.optional()` / `.nullish()` / `.default()` wrappers. `.omit()` lives on the inner `ZodObject`,
569
+ * not on those `ZodNullable` / `ZodOptional` / `ZodDefault` wrappers, so the omit has to unwrap
570
+ * down to the object first. This mirrors plugin-ts emitting `Omit<NonNullable<T>, …>`. Inline
571
+ * objects need no unwrap because the printer adds their modifiers after `.omit()`.
572
+ */
573
+ function omitUnwrapChain(node) {
574
+ const ref = ast.narrowSchema(node, "ref");
575
+ if (!ref) return "";
576
+ const target = ref.schema ?? ref;
577
+ const depth = modifierDepth(target) + (target.default !== void 0 ? 1 : 0);
578
+ return ".unwrap()".repeat(depth);
579
+ }
517
580
  /**
518
581
  * Apply nullable / optional / nullish modifiers using the functional `zod/mini` API
519
582
  * (`z.nullable()`, `z.optional()`, `z.nullish()`).
520
583
  */
521
- function applyMiniModifiers({ value, nullable, optional, nullish, defaultValue }) {
584
+ function applyMiniModifiers({ value, schema, nullable, optional, nullish, defaultValue }) {
522
585
  const withModifier = (() => {
523
586
  if (nullish) return `z.nullish(${value})`;
524
587
  const withNullable = nullable ? `z.nullable(${value})` : value;
525
588
  return optional ? `z.optional(${withNullable})` : withNullable;
526
589
  })();
527
- return defaultValue !== void 0 ? `z._default(${withModifier}, ${formatDefault(defaultValue)})` : withModifier;
590
+ const literal = defaultValue !== void 0 ? defaultLiteral(schema, defaultValue) : null;
591
+ return literal !== null ? `z._default(${withModifier}, ${literal})` : withModifier;
528
592
  }
529
593
  //#endregion
530
594
  //#region src/printers/printerZod.ts
531
- function strictOneOfMember$1(member, node) {
595
+ function strictOneOfMember$1(member, node, cyclicSchemas) {
532
596
  if (node.type === "object" && node.additionalProperties === void 0) return `${member}.strict()`;
533
597
  if (node.type === "ref") {
534
598
  if (member.startsWith("z.lazy(")) return member;
599
+ const refName = (node.ref ? extractRefName(node.ref) : void 0) ?? node.name;
600
+ if (refName && cyclicSchemas?.has(refName)) return member;
535
601
  const schema = syncSchemaRef(node);
602
+ if (schema.nullable || schema.optional || node.nullable || node.optional) return member;
536
603
  if (schema.type === "object" && (schema.additionalProperties === void 0 || schema.additionalProperties === false)) return `${member}.strict()`;
537
604
  }
538
605
  return member;
@@ -562,6 +629,7 @@ function getMemberConstraint({ member, regexType }) {
562
629
  * ```
563
630
  */
564
631
  const printerZod = ast.createPrinter((options) => {
632
+ const cyclicSchemaNames = options.cyclicSchemas;
565
633
  return {
566
634
  name: "zod",
567
635
  options,
@@ -631,11 +699,11 @@ const printerZod = ast.createPrinter((options) => {
631
699
  if (literals.length === 1) return literals[0];
632
700
  return `z.union([${literals.join(", ")}])`;
633
701
  }
634
- return `z.enum([${nonNullValues.map(formatLiteral).join(", ")}])`;
702
+ return buildEnum(nonNullValues);
635
703
  },
636
704
  ref(node) {
637
705
  if (!node.name) return null;
638
- const refName = node.ref ? extractRefName(node.ref) ?? node.name : node.name;
706
+ const refName = node.ref ? this.options.nameMapping?.get(node.ref) ?? extractRefName(node.ref) ?? node.name : node.name;
639
707
  const useInputVariant = node.ref != null && this.options.direction === "input" && containsCodec(node);
640
708
  const resolvedName = node.ref ? useInputVariant ? this.options.resolver?.resolveInputSchemaName(refName) ?? refName : this.options.resolver?.default(refName, "function") ?? refName : node.name;
641
709
  if (node.ref && this.options.cyclicSchemas?.has(refName)) return `z.lazy(() => ${resolvedName})`;
@@ -660,6 +728,7 @@ const printerZod = ast.createPrinter((options) => {
660
728
  const descriptionToApply = schema.type !== "ref" && meta.type === "ref" ? void 0 : meta.description;
661
729
  const value = applyModifiers({
662
730
  value: wrappedOutput,
731
+ schema,
663
732
  nullable: meta.nullable,
664
733
  optional: schema.optional || property.required === false,
665
734
  nullish: schema.nullish,
@@ -709,10 +778,11 @@ const printerZod = ast.createPrinter((options) => {
709
778
  },
710
779
  union(node) {
711
780
  const nodeMembers = node.members ?? [];
712
- const members = mapSchemaMembers(node, (memberNode) => this.transform(memberNode)).map(({ schema, output }) => output && node.strategy === "one" ? strictOneOfMember$1(output, schema) : output).filter(Boolean);
781
+ const members = mapSchemaMembers(node, (memberNode) => this.transform(memberNode)).map(({ schema, output }) => output && node.strategy === "one" ? strictOneOfMember$1(output, schema, cyclicSchemaNames) : output).filter(Boolean);
713
782
  if (members.length === 0) return "";
714
783
  if (members.length === 1) return members[0];
715
- if (node.discriminatorPropertyName && !nodeMembers.some((m) => m.type === "intersection")) return `z.discriminatedUnion(${stringify(node.discriminatorPropertyName)}, ${buildList(members)})`;
784
+ const allDiscriminable = nodeMembers.every((m) => (m.type === "ref" ? syncSchemaRef(m) : m).type !== "intersection");
785
+ if (node.discriminatorPropertyName && allDiscriminable) return `z.discriminatedUnion(${stringify(node.discriminatorPropertyName)}, ${buildList(members)})`;
716
786
  return `z.union(${buildList(members)})`;
717
787
  },
718
788
  intersection(node) {
@@ -742,10 +812,13 @@ const printerZod = ast.createPrinter((options) => {
742
812
  return applyModifiers({
743
813
  value: (() => {
744
814
  if (!keysToOmit?.length || meta.primitive !== "object" || meta.type === "union" && meta.discriminatorPropertyName) return transformed;
815
+ const unwrap = omitUnwrapChain(node);
816
+ const omit = `.omit({ ${keysToOmit.map((k) => `"${k}": true`).join(", ")} })`;
745
817
  const lazyMatch = transformed.match(/^z\.lazy\(\(\)\s*=>\s*(.+)\)$/);
746
- if (lazyMatch) return `z.lazy(() => ${lazyMatch[1]}.omit({ ${keysToOmit.map((k) => `"${k}": true`).join(", ")} }))`;
747
- return `${transformed}.omit({ ${keysToOmit.map((k) => `"${k}": true`).join(", ")} })`;
818
+ if (lazyMatch) return `z.lazy(() => ${lazyMatch[1]}${unwrap}${omit})`;
819
+ return `${transformed}${unwrap}${omit}`;
748
820
  })(),
821
+ schema: node,
749
822
  nullable: meta.nullable,
750
823
  optional: meta.optional,
751
824
  nullish: meta.nullish,
@@ -850,11 +923,11 @@ const printerZodMini = ast.createPrinter((options) => {
850
923
  if (literals.length === 1) return literals[0];
851
924
  return `z.union([${literals.join(", ")}])`;
852
925
  }
853
- return `z.enum([${nonNullValues.map(formatLiteral).join(", ")}])`;
926
+ return buildEnum(nonNullValues);
854
927
  },
855
928
  ref(node) {
856
929
  if (!node.name) return null;
857
- const refName = node.ref ? extractRefName(node.ref) ?? node.name : node.name;
930
+ const refName = node.ref ? this.options.nameMapping?.get(node.ref) ?? extractRefName(node.ref) ?? node.name : node.name;
858
931
  const resolvedName = node.ref ? this.options.resolver?.default(refName, "function") ?? refName : node.name;
859
932
  if (node.ref && this.options.cyclicSchemas?.has(refName)) return `z.lazy(() => ${resolvedName})`;
860
933
  return resolvedName;
@@ -876,6 +949,7 @@ const printerZodMini = ast.createPrinter((options) => {
876
949
  output: baseOutput,
877
950
  schema
878
951
  }) || baseOutput : baseOutput,
952
+ schema,
879
953
  nullable: meta.nullable,
880
954
  optional: schema.optional || property.required === false,
881
955
  nullish: schema.nullish,
@@ -924,7 +998,8 @@ const printerZodMini = ast.createPrinter((options) => {
924
998
  const members = mapSchemaMembers(node, (memberNode) => this.transform(memberNode)).map(({ schema, output }) => output && node.strategy === "one" ? strictOneOfMember(output, schema) : output).filter(Boolean);
925
999
  if (members.length === 0) return "";
926
1000
  if (members.length === 1) return members[0];
927
- if (node.discriminatorPropertyName && !nodeMembers.some((m) => m.type === "intersection")) return `z.discriminatedUnion(${stringify(node.discriminatorPropertyName)}, ${buildList(members)})`;
1001
+ const allDiscriminable = nodeMembers.every((m) => (m.type === "ref" ? syncSchemaRef(m) : m).type !== "intersection");
1002
+ if (node.discriminatorPropertyName && allDiscriminable) return `z.discriminatedUnion(${stringify(node.discriminatorPropertyName)}, ${buildList(members)})`;
928
1003
  return `z.union(${buildList(members)})`;
929
1004
  },
930
1005
  intersection(node) {
@@ -954,10 +1029,13 @@ const printerZodMini = ast.createPrinter((options) => {
954
1029
  return applyMiniModifiers({
955
1030
  value: (() => {
956
1031
  if (!keysToOmit?.length || meta.primitive !== "object" || meta.type === "union" && meta.discriminatorPropertyName) return transformed;
1032
+ const unwrap = omitUnwrapChain(node);
1033
+ const omit = `.omit({ ${keysToOmit.map((k) => `"${k}": true`).join(", ")} })`;
957
1034
  const lazyMatch = transformed.match(/^z\.lazy\(\(\)\s*=>\s*(.+)\)$/);
958
- if (lazyMatch) return `z.lazy(() => ${lazyMatch[1]}.omit({ ${keysToOmit.map((k) => `"${k}": true`).join(", ")} }))`;
959
- return `${transformed}.omit({ ${keysToOmit.map((k) => `"${k}": true`).join(", ")} })`;
1035
+ if (lazyMatch) return `z.lazy(() => ${lazyMatch[1]}${unwrap}${omit})`;
1036
+ return `${transformed}${unwrap}${omit}`;
960
1037
  })(),
1038
+ schema: node,
961
1039
  nullable: meta.nullable,
962
1040
  optional: meta.optional,
963
1041
  nullish: meta.nullish,
@@ -1079,6 +1157,7 @@ const zodGenerator = defineGenerator({
1079
1157
  })
1080
1158
  };
1081
1159
  const inferTypeName = inferred ? resolver.resolveSchemaTypeName(node.name) : null;
1160
+ const nameMapping = adapter.options.nameMapping;
1082
1161
  const stdPrinters = mini ? null : getStdPrinters(resolver, {
1083
1162
  coercion,
1084
1163
  guidType,
@@ -1086,6 +1165,7 @@ const zodGenerator = defineGenerator({
1086
1165
  dateType,
1087
1166
  wrapOutput,
1088
1167
  cyclicSchemas,
1168
+ nameMapping,
1089
1169
  nodes: printer?.nodes
1090
1170
  });
1091
1171
  const schemaPrinter = mini ? getMiniPrinter(resolver, {
@@ -1093,6 +1173,7 @@ const zodGenerator = defineGenerator({
1093
1173
  regexType,
1094
1174
  wrapOutput,
1095
1175
  cyclicSchemas,
1176
+ nameMapping,
1096
1177
  nodes: printer?.nodes
1097
1178
  }) : stdPrinters.output;
1098
1179
  return /* @__PURE__ */ jsxs(File, {
@@ -1134,13 +1215,15 @@ const zodGenerator = defineGenerator({
1134
1215
  name: meta.name,
1135
1216
  node,
1136
1217
  printer: schemaPrinter,
1137
- inferTypeName
1218
+ inferTypeName,
1219
+ cyclic: cyclicSchemas.has(node.name)
1138
1220
  }),
1139
1221
  hasCodec && stdPrinters && /* @__PURE__ */ jsx(Zod, {
1140
1222
  name: resolver.resolveInputSchemaName(node.name),
1141
1223
  node,
1142
1224
  printer: stdPrinters.input,
1143
- inferTypeName: inferred ? resolver.resolveInputSchemaTypeName(node.name) : null
1225
+ inferTypeName: inferred ? resolver.resolveInputSchemaTypeName(node.name) : null,
1226
+ cyclic: cyclicSchemas.has(node.name)
1144
1227
  })
1145
1228
  ]
1146
1229
  });
@@ -1163,6 +1246,7 @@ const zodGenerator = defineGenerator({
1163
1246
  group: group ?? void 0
1164
1247
  }) };
1165
1248
  const cyclicSchemas = new Set(ctx.meta.circularNames);
1249
+ const nameMapping = adapter.options.nameMapping;
1166
1250
  function renderSchemaEntry({ schema, name, keysToOmit, direction = "output" }) {
1167
1251
  if (!schema) return null;
1168
1252
  const inferTypeName = inferred ? resolver.resolveTypeName(name) : null;
@@ -1185,12 +1269,14 @@ const zodGenerator = defineGenerator({
1185
1269
  resolver,
1186
1270
  keysToOmit,
1187
1271
  cyclicSchemas,
1272
+ nameMapping,
1188
1273
  nodes: printer?.nodes
1189
1274
  }) : getMiniPrinter(resolver, {
1190
1275
  guidType,
1191
1276
  regexType,
1192
1277
  wrapOutput,
1193
1278
  cyclicSchemas,
1279
+ nameMapping,
1194
1280
  nodes: printer?.nodes
1195
1281
  }) : keysToOmit?.length ? printerZod({
1196
1282
  coercion,
@@ -1201,6 +1287,7 @@ const zodGenerator = defineGenerator({
1201
1287
  resolver,
1202
1288
  keysToOmit,
1203
1289
  cyclicSchemas,
1290
+ nameMapping,
1204
1291
  nodes: printer?.nodes,
1205
1292
  direction
1206
1293
  }) : getStdPrinters(resolver, {
@@ -1210,6 +1297,7 @@ const zodGenerator = defineGenerator({
1210
1297
  dateType,
1211
1298
  wrapOutput,
1212
1299
  cyclicSchemas,
1300
+ nameMapping,
1213
1301
  nodes: printer?.nodes
1214
1302
  })[direction];
1215
1303
  return /* @__PURE__ */ jsxs(Fragment, { children: [imports.map((imp) => /* @__PURE__ */ jsx(File.Import, {
@@ -1224,7 +1312,8 @@ const zodGenerator = defineGenerator({
1224
1312
  name,
1225
1313
  node: schema,
1226
1314
  printer: schemaPrinter,
1227
- inferTypeName
1315
+ inferTypeName,
1316
+ cyclic: false
1228
1317
  })] });
1229
1318
  }
1230
1319
  function buildContentTypeVariants(entries, baseName, decorate, direction) {
@@ -1444,7 +1533,7 @@ const pluginZodName = "plugin-zod";
1444
1533
  * Generates Zod v4 schemas from an OpenAPI spec. Use them to validate API
1445
1534
  * responses at runtime, build form schemas, or feed back into router libraries
1446
1535
  * that consume Zod (tRPC, Hono, Elysia). Pair with `@kubb/plugin-axios` or
1447
- * `@kubb/plugin-fetch` and set the client's `parser: 'zod'` to validate every response
1536
+ * `@kubb/plugin-fetch` and set the client's `validator: 'zod'` to validate every response
1448
1537
  * automatically.
1449
1538
  *
1450
1539
  * @example