@kubb/plugin-zod 5.0.0-beta.4 → 5.0.0-beta.56

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.js CHANGED
@@ -1,5 +1,6 @@
1
- import "./chunk--u3MIqq1.js";
1
+ import { t as __name } from "./chunk-C0LytTxp.js";
2
2
  import { ast, defineGenerator, definePlugin, defineResolver } from "@kubb/core";
3
+ import { buildList, buildObject, extractRefName, objectKey, stringify, stringifyObject, toRegExpString } from "@kubb/ast/utils";
3
4
  import { Const, File, Type, jsxRenderer } from "@kubb/renderer-jsx";
4
5
  import { Fragment, jsx, jsxs } from "@kubb/renderer-jsx/jsx-runtime";
5
6
  //#region ../../internals/utils/src/casing.ts
@@ -13,117 +14,261 @@ import { Fragment, jsx, jsxs } from "@kubb/renderer-jsx/jsx-runtime";
13
14
  function toCamelOrPascal(text, pascal) {
14
15
  return text.trim().replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/(\d)([a-z])/g, "$1 $2").split(/[\s\-_./\\:]+/).filter(Boolean).map((word, i) => {
15
16
  if (word.length > 1 && word === word.toUpperCase()) return word;
16
- if (i === 0 && !pascal) return word.charAt(0).toLowerCase() + word.slice(1);
17
- return word.charAt(0).toUpperCase() + word.slice(1);
17
+ return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
18
18
  }).join("").replace(/[^a-zA-Z0-9]/g, "");
19
19
  }
20
20
  /**
21
- * Splits `text` on `.` and applies `transformPart` to each segment.
22
- * The last segment receives `isLast = true`, all earlier segments receive `false`.
23
- * Segments are joined with `/` to form a file path.
24
- *
25
- * Only splits on dots followed by a letter so that version numbers
26
- * embedded in operationIds (e.g. `v2025.0`) are kept intact.
27
- */
28
- function applyToFileParts(text, transformPart) {
29
- const parts = text.split(/\.(?=[a-zA-Z])/);
30
- return parts.map((part, i) => transformPart(part, i === parts.length - 1)).join("/");
31
- }
32
- /**
33
21
  * Converts `text` to camelCase.
34
- * When `isFile` is `true`, dot-separated segments are each cased independently and joined with `/`.
35
22
  *
36
- * @example
37
- * camelCase('hello-world') // 'helloWorld'
38
- * camelCase('pet.petId', { isFile: true }) // 'pet/petId'
23
+ * @example Word boundaries
24
+ * `camelCase('hello-world') // 'helloWorld'`
25
+ *
26
+ * @example With a prefix
27
+ * `camelCase('tag', { prefix: 'create' }) // 'createTag'`
39
28
  */
40
- function camelCase(text, { isFile, prefix = "", suffix = "" } = {}) {
41
- if (isFile) return applyToFileParts(text, (part, isLast) => camelCase(part, isLast ? {
42
- prefix,
43
- suffix
44
- } : {}));
29
+ function camelCase(text, { prefix = "", suffix = "" } = {}) {
45
30
  return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
46
31
  }
47
32
  /**
48
33
  * Converts `text` to PascalCase.
49
- * When `isFile` is `true`, the last dot-separated segment is PascalCased and earlier segments are camelCased.
50
34
  *
51
- * @example
52
- * pascalCase('hello-world') // 'HelloWorld'
53
- * pascalCase('pet.petId', { isFile: true }) // 'pet/PetId'
35
+ * @example Word boundaries
36
+ * `pascalCase('hello-world') // 'HelloWorld'`
37
+ *
38
+ * @example With a suffix
39
+ * `pascalCase('tag', { suffix: 'schema' }) // 'TagSchema'`
54
40
  */
55
- function pascalCase(text, { isFile, prefix = "", suffix = "" } = {}) {
56
- if (isFile) return applyToFileParts(text, (part, isLast) => isLast ? pascalCase(part, {
57
- prefix,
58
- suffix
59
- }) : camelCase(part));
41
+ function pascalCase(text, { prefix = "", suffix = "" } = {}) {
60
42
  return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
61
43
  }
62
44
  //#endregion
63
- //#region ../../internals/utils/src/string.ts
45
+ //#region ../../internals/utils/src/fs.ts
64
46
  /**
65
- * Strips a single matching pair of `"..."`, `'...'`, or `` `...` `` from both ends of `text`.
66
- * Returns the string unchanged when no balanced quote pair is found.
47
+ * Builds a nested file path from a dotted name. Splits on dots that precede a letter
48
+ * (so version numbers embedded in operationIds like `v2025.0` stay intact), camelCases
49
+ * every earlier segment, applies `caseLast` to the final segment, and joins with `/`.
67
50
  *
68
- * @example
69
- * trimQuotes('"hello"') // 'hello'
70
- * trimQuotes('hello') // 'hello'
51
+ * Empty segments are dropped before joining. They arise when the name starts with a dot
52
+ * followed by a letter (e.g. `..Schema` splits into `['..', 'Schema']` and `'..'` cases to
53
+ * an empty string). Without this a leading `/` would form, which `path.resolve` reads as an
54
+ * absolute path, letting generated files escape the configured output directory.
55
+ *
56
+ * @example Nested path from a dotted name
57
+ * `toFilePath('pet.petId') // 'pet/petId'`
58
+ *
59
+ * @example PascalCase the final segment
60
+ * `toFilePath('pet.Pet', pascalCase) // 'pet/Pet'`
61
+ *
62
+ * @example Suffix applied to the final segment only
63
+ * `toFilePath('tag.tag', (part) => camelCase(part, { suffix: 'schema' })) // 'tag/tagSchema'`
71
64
  */
72
- function trimQuotes(text) {
73
- if (text.length >= 2) {
74
- const first = text[0];
75
- const last = text[text.length - 1];
76
- if (first === "\"" && last === "\"" || first === "'" && last === "'" || first === "`" && last === "`") return text.slice(1, -1);
77
- }
78
- return text;
65
+ function toFilePath(name, caseLast = camelCase) {
66
+ const parts = name.split(/\.(?=[a-zA-Z])/);
67
+ return parts.map((part, i) => i === parts.length - 1 ? caseLast(part) : camelCase(part)).filter(Boolean).join("/");
79
68
  }
80
69
  //#endregion
81
- //#region ../../internals/utils/src/object.ts
70
+ //#region ../../internals/utils/src/reserved.ts
82
71
  /**
83
- * Serializes a primitive value to a JSON string literal, stripping any surrounding quote characters first.
72
+ * JavaScript and Java reserved words.
73
+ * @link https://github.com/jonschlinkert/reserved/blob/master/index.js
74
+ */
75
+ const reservedWords = new Set([
76
+ "abstract",
77
+ "arguments",
78
+ "boolean",
79
+ "break",
80
+ "byte",
81
+ "case",
82
+ "catch",
83
+ "char",
84
+ "class",
85
+ "const",
86
+ "continue",
87
+ "debugger",
88
+ "default",
89
+ "delete",
90
+ "do",
91
+ "double",
92
+ "else",
93
+ "enum",
94
+ "eval",
95
+ "export",
96
+ "extends",
97
+ "false",
98
+ "final",
99
+ "finally",
100
+ "float",
101
+ "for",
102
+ "function",
103
+ "goto",
104
+ "if",
105
+ "implements",
106
+ "import",
107
+ "in",
108
+ "instanceof",
109
+ "int",
110
+ "interface",
111
+ "let",
112
+ "long",
113
+ "native",
114
+ "new",
115
+ "null",
116
+ "package",
117
+ "private",
118
+ "protected",
119
+ "public",
120
+ "return",
121
+ "short",
122
+ "static",
123
+ "super",
124
+ "switch",
125
+ "synchronized",
126
+ "this",
127
+ "throw",
128
+ "throws",
129
+ "transient",
130
+ "true",
131
+ "try",
132
+ "typeof",
133
+ "var",
134
+ "void",
135
+ "volatile",
136
+ "while",
137
+ "with",
138
+ "yield",
139
+ "Array",
140
+ "Date",
141
+ "hasOwnProperty",
142
+ "Infinity",
143
+ "isFinite",
144
+ "isNaN",
145
+ "isPrototypeOf",
146
+ "length",
147
+ "Math",
148
+ "name",
149
+ "NaN",
150
+ "Number",
151
+ "Object",
152
+ "prototype",
153
+ "String",
154
+ "toString",
155
+ "undefined",
156
+ "valueOf"
157
+ ]);
158
+ /**
159
+ * Returns `true` when `name` is a syntactically valid JavaScript variable name.
84
160
  *
85
161
  * @example
86
- * stringify('hello') // '"hello"'
87
- * stringify('"hello"') // '"hello"'
162
+ * ```ts
163
+ * isValidVarName('status') // true
164
+ * isValidVarName('class') // false (reserved word)
165
+ * isValidVarName('42foo') // false (starts with digit)
166
+ * ```
88
167
  */
89
- function stringify(value) {
90
- if (value === void 0 || value === null) return "\"\"";
91
- return JSON.stringify(trimQuotes(value.toString()));
168
+ function isValidVarName(name) {
169
+ if (!name || reservedWords.has(name)) return false;
170
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
92
171
  }
93
172
  /**
94
- * Converts a plain object into a multiline key-value string suitable for embedding in generated code.
95
- * Nested objects are recursively stringified with indentation.
173
+ * Returns `name` when it's a syntactically valid JavaScript variable name,
174
+ * otherwise prefixes it with `_` so the result is a valid identifier.
175
+ *
176
+ * Useful for sanitizing OpenAPI schema names or operation IDs that start with
177
+ * a digit (e.g. `409`, `504AccountCancel`) before using them as exported
178
+ * variable, type, or function names.
96
179
  *
97
180
  * @example
98
- * stringifyObject({ foo: 'bar', nested: { a: 1 } })
99
- * // 'foo: bar,\nnested: {\n a: 1\n }'
181
+ * ```ts
182
+ * ensureValidVarName('409') // '_409'
183
+ * ensureValidVarName('504AccountCancel') // '_504AccountCancel'
184
+ * ensureValidVarName('Pet') // 'Pet'
185
+ * ensureValidVarName('class') // '_class'
186
+ * ```
100
187
  */
101
- function stringifyObject(value) {
102
- return Object.entries(value).map(([key, val]) => {
103
- if (val !== null && typeof val === "object") return `${key}: {\n ${stringifyObject(val)}\n }`;
104
- return `${key}: ${val}`;
105
- }).filter(Boolean).join(",\n");
188
+ function ensureValidVarName(name) {
189
+ if (!name || isValidVarName(name)) return name;
190
+ return `_${name}`;
106
191
  }
107
192
  //#endregion
108
- //#region ../../internals/utils/src/regexp.ts
193
+ //#region ../../internals/shared/src/operation.ts
194
+ /**
195
+ * Maps a content type to the PascalCase suffix used to name per-content-type variants
196
+ * (e.g. `application/json` → `Json`, `application/xml` → `Xml`, `multipart/form-data` → `FormData`).
197
+ */
198
+ function getContentTypeSuffix(contentType) {
199
+ const baseType = contentType.split(";")[0].trim();
200
+ if (baseType === "application/json") return "Json";
201
+ if (baseType === "multipart/form-data") return "FormData";
202
+ if (baseType === "application/x-www-form-urlencoded") return "FormUrlEncoded";
203
+ const parts = (baseType.split("/").pop() ?? baseType).split(/[^a-zA-Z0-9]+/).filter(Boolean);
204
+ if (parts.length === 0) return "Unknown";
205
+ return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
206
+ }
109
207
  /**
110
- * Converts a pattern string into a `new RegExp(...)` constructor call or a regex literal string.
111
- * Inline flags expressed as `^(?im)` prefixes are extracted and applied to the resulting expression.
112
- * Pass `null` as the second argument to emit a `/pattern/flags` literal instead.
208
+ * Appends a content-type suffix to a base name, keeping a trailing `Data` segment last
209
+ * (e.g. `AddPetData` + `Json` `AddPetJsonData`, `AddPetStatus200` + `Xml` `AddPetStatus200Xml`).
210
+ */
211
+ function getPerContentTypeName(baseName, suffix) {
212
+ if (baseName.endsWith("Data")) return suffix.endsWith("Data") ? baseName.slice(0, -4) + suffix : `${baseName.slice(0, -4)}${suffix}Data`;
213
+ return baseName + suffix;
214
+ }
215
+ /**
216
+ * Resolves per-content-type variant names for a set of content entries, deduplicating suffix
217
+ * collisions with a numeric counter. Entries without a schema are skipped. The returned `suffix` is
218
+ * the final (possibly counter-augmented) value, so callers can derive parallel names in another
219
+ * namespace (e.g. plugin-faker deriving the matching plugin-ts type name).
220
+ */
221
+ function resolveContentTypeVariants(entries, baseName) {
222
+ const usedNames = /* @__PURE__ */ new Set();
223
+ return entries.filter((entry) => entry.schema).map((entry) => {
224
+ const baseSuffix = getContentTypeSuffix(entry.contentType);
225
+ let suffix = baseSuffix;
226
+ let name = getPerContentTypeName(baseName, suffix);
227
+ let counter = 2;
228
+ while (usedNames.has(name)) {
229
+ suffix = `${baseSuffix}${counter++}`;
230
+ name = getPerContentTypeName(baseName, suffix);
231
+ }
232
+ usedNames.add(name);
233
+ return {
234
+ name,
235
+ suffix,
236
+ schema: entry.schema,
237
+ keysToOmit: entry.keysToOmit,
238
+ contentType: entry.contentType
239
+ };
240
+ });
241
+ }
242
+ //#endregion
243
+ //#region ../../internals/shared/src/group.ts
244
+ /**
245
+ * Builds the `group` config a Kubb plugin passes to `ctx.setOptions`, applying the
246
+ * shared default naming so every plugin groups output consistently:
247
+ *
248
+ * - `path` groups use the second path segment (`/pet/findByStatus` → `pet`).
249
+ * - other groups use the camelCased group (`pet store` → `petStore`).
250
+ *
251
+ * A user-provided `group.name` always wins over the default namer, so callers stay in
252
+ * control of their output folders. Returns `null` when grouping is disabled, matching the
253
+ * per-plugin convention.
254
+ *
255
+ * @param group - The user-supplied group option, or `undefined` to disable grouping.
113
256
  *
114
257
  * @example
115
- * toRegExpString('^(?im)foo') // → 'new RegExp("foo", "im")'
116
- * toRegExpString('^(?im)foo', null) // '/foo/im'
258
+ * ```ts
259
+ * createGroupConfig(group) // shared across every plugin
260
+ * ```
117
261
  */
118
- function toRegExpString(text, func = "RegExp") {
119
- const raw = trimQuotes(text);
120
- const match = raw.match(/^\^(\(\?([igmsuy]+)\))/i);
121
- const replacementTarget = match?.[1] ?? "";
122
- const matchedFlags = match?.[2];
123
- const cleaned = raw.replace(/^\\?\//, "").replace(/\\?\/$/, "").replace(replacementTarget, "");
124
- const { source, flags } = new RegExp(cleaned, matchedFlags);
125
- if (func === null) return `/${source}/${flags}`;
126
- return `new ${func}(${JSON.stringify(source)}${flags ? `, ${JSON.stringify(flags)}` : ""})`;
262
+ function createGroupConfig(group) {
263
+ if (!group) return null;
264
+ const defaultName = (ctx) => {
265
+ if (group.type === "path") return `${ctx.group.split("/")[1]}`;
266
+ return camelCase(ctx.group);
267
+ };
268
+ return {
269
+ ...group,
270
+ name: group.name ? group.name : defaultName
271
+ };
127
272
  }
128
273
  //#endregion
129
274
  //#region src/components/Operations.tsx
@@ -133,6 +278,7 @@ function Operations({ name, operations }) {
133
278
  return prev;
134
279
  }, {});
135
280
  const pathsJSON = operations.reduce((prev, acc) => {
281
+ if (!ast.isHttpOperationNode(acc.node)) return prev;
136
282
  prev[`"${acc.node.path}"`] = {
137
283
  ...prev[`"${acc.node.path}"`] ?? {},
138
284
  [acc.node.method]: `operations["${acc.node.operationId}"]`
@@ -242,6 +388,61 @@ function shouldCoerce(coercion, type) {
242
388
  return !!coercion[type];
243
389
  }
244
390
  /**
391
+ * Registered codecs, checked in order.
392
+ */
393
+ const codecs = [{
394
+ matches(node) {
395
+ return node.type === "date" && node.representation === "date";
396
+ },
397
+ decode(node) {
398
+ return node.format === "date" ? "z.iso.date().transform((value) => new Date(value))" : "z.iso.datetime().transform((value) => new Date(value))";
399
+ },
400
+ encode(node) {
401
+ return node.format === "date" ? "z.date().transform((value) => value.toISOString().slice(0, 10))" : "z.date().transform((value) => value.toISOString())";
402
+ }
403
+ }];
404
+ /**
405
+ * Returns the codec for this node, or `undefined` when the node needs no
406
+ * encode/decode (its wire and runtime types match).
407
+ */
408
+ function getCodec(node) {
409
+ if (!node) return void 0;
410
+ return codecs.find((codec) => codec.matches(node));
411
+ }
412
+ /**
413
+ * Returns `true` when the node itself is encoded/decoded by a codec.
414
+ */
415
+ function hasCodec(node) {
416
+ return getCodec(node) !== void 0;
417
+ }
418
+ /**
419
+ * Returns `true` when the schema transitively contains a codec node —
420
+ * a value whose runtime type differs from its wire type (see {@link hasCodec}),
421
+ * so it must be decoded (response) or encoded (request) at the validation boundary.
422
+ * `$ref`s are followed via their resolved schema; a `seen` set guards cycles.
423
+ */
424
+ function containsCodec(node, seen = /* @__PURE__ */ new Set()) {
425
+ if (!node) return false;
426
+ if (hasCodec(node)) return true;
427
+ if (node.type === "ref") {
428
+ if (!node.ref) return false;
429
+ const refName = extractRefName(node.ref);
430
+ if (refName) {
431
+ if (seen.has(refName)) return false;
432
+ seen.add(refName);
433
+ }
434
+ const resolved = ast.syncSchemaRef(node);
435
+ if (resolved.type === "ref") return false;
436
+ return containsCodec(resolved, seen);
437
+ }
438
+ const children = [];
439
+ if ("properties" in node && node.properties) children.push(...node.properties.map((prop) => prop.schema));
440
+ if ("items" in node && node.items) children.push(...node.items);
441
+ if ("members" in node && node.members) children.push(...node.members);
442
+ if ("additionalProperties" in node && node.additionalProperties && node.additionalProperties !== true) children.push(node.additionalProperties);
443
+ return children.some((child) => containsCodec(child, seen));
444
+ }
445
+ /**
245
446
  * Collects all resolved schema names for an operation's parameters and responses
246
447
  * into a single lookup object, useful for building imports and type references.
247
448
  */
@@ -261,11 +462,11 @@ function buildSchemaNames(node, { params, resolver }) {
261
462
  }
262
463
  responses["default"] = resolver.resolveResponseName(node);
263
464
  return {
264
- request: node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : void 0,
465
+ request: node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : null,
265
466
  parameters: {
266
- path: pathParam ? resolver.resolvePathParamsName(node, pathParam) : void 0,
267
- query: queryParam ? resolver.resolveQueryParamsName(node, queryParam) : void 0,
268
- header: headerParam ? resolver.resolveHeaderParamsName(node, headerParam) : void 0
467
+ path: pathParam ? resolver.resolvePathParamsName(node, pathParam) : null,
468
+ query: queryParam ? resolver.resolveQueryParamsName(node, queryParam) : null,
469
+ header: headerParam ? resolver.resolveHeaderParamsName(node, headerParam) : null
269
470
  },
270
471
  responses,
271
472
  errors
@@ -339,30 +540,44 @@ function lengthChecksMini({ min, max, pattern }) {
339
540
  * to a schema value string using the chainable Zod v4 API.
340
541
  */
341
542
  function applyModifiers({ value, nullable, optional, nullish, defaultValue, description }) {
342
- let result = value;
343
- if (nullish || nullable && optional) result = `${result}.nullish()`;
344
- else if (optional) result = `${result}.optional()`;
345
- else if (nullable) result = `${result}.nullable()`;
346
- if (defaultValue !== void 0) result = `${result}.default(${formatDefault(defaultValue)})`;
347
- if (description) result = `${result}.describe(${stringify(description)})`;
348
- return result;
543
+ const withModifier = (() => {
544
+ if (nullish || nullable && optional) return `${value}.nullish()`;
545
+ if (optional) return `${value}.optional()`;
546
+ if (nullable) return `${value}.nullable()`;
547
+ return value;
548
+ })();
549
+ const withDefault = defaultValue !== void 0 ? `${withModifier}.default(${formatDefault(defaultValue)})` : withModifier;
550
+ return description ? `${withDefault}.describe(${stringify(description)})` : withDefault;
349
551
  }
350
552
  /**
351
553
  * Apply nullable / optional / nullish modifiers using the functional `zod/mini` API
352
554
  * (`z.nullable()`, `z.optional()`, `z.nullish()`).
353
555
  */
354
556
  function applyMiniModifiers({ value, nullable, optional, nullish, defaultValue }) {
355
- let result = value;
356
- if (nullish) result = `z.nullish(${result})`;
357
- else {
358
- if (nullable) result = `z.nullable(${result})`;
359
- if (optional) result = `z.optional(${result})`;
360
- }
361
- if (defaultValue !== void 0) result = `z._default(${result}, ${formatDefault(defaultValue)})`;
362
- return result;
557
+ const withModifier = (() => {
558
+ if (nullish) return `z.nullish(${value})`;
559
+ const withNullable = nullable ? `z.nullable(${value})` : value;
560
+ return optional ? `z.optional(${withNullable})` : withNullable;
561
+ })();
562
+ return defaultValue !== void 0 ? `z._default(${withModifier}, ${formatDefault(defaultValue)})` : withModifier;
363
563
  }
364
564
  //#endregion
365
565
  //#region src/printers/printerZod.ts
566
+ function strictOneOfMember$1(member, node) {
567
+ if (node.type === "object" && node.additionalProperties === void 0) return `${member}.strict()`;
568
+ if (node.type === "ref") {
569
+ if (member.startsWith("z.lazy(")) return member;
570
+ const schema = ast.syncSchemaRef(node);
571
+ if (schema.type === "object" && (schema.additionalProperties === void 0 || schema.additionalProperties === false)) return `${member}.strict()`;
572
+ }
573
+ return member;
574
+ }
575
+ __name(strictOneOfMember$1, "strictOneOfMember");
576
+ function getMemberConstraint(member) {
577
+ if (member.primitive === "string") return lengthConstraints(ast.narrowSchema(member, "string") ?? {}) || void 0;
578
+ if (member.primitive === "number" || member.primitive === "integer") return numberConstraints(ast.narrowSchema(member, "number") ?? ast.narrowSchema(member, "integer") ?? {}) || void 0;
579
+ if (member.primitive === "array") return lengthConstraints(ast.narrowSchema(member, "array") ?? {}) || void 0;
580
+ }
366
581
  /**
367
582
  * Zod v4 printer built with `definePrinter`.
368
583
  *
@@ -399,8 +614,9 @@ const printerZod = ast.definePrinter((options) => {
399
614
  return shouldCoerce(this.options.coercion, "numbers") ? "z.coerce.bigint()" : "z.bigint()";
400
615
  },
401
616
  date(node) {
402
- if (node.representation === "string") return "z.iso.date()";
403
- return shouldCoerce(this.options.coercion, "dates") ? "z.coerce.date()" : "z.date()";
617
+ const codec = getCodec(node);
618
+ if (codec) return this.options.direction === "input" ? codec.encode(node) : codec.decode(node);
619
+ return "z.iso.date()";
404
620
  },
405
621
  datetime(node) {
406
622
  const offset = node.offset || this.options.dateType === "stringOffset";
@@ -435,29 +651,30 @@ const printerZod = ast.definePrinter((options) => {
435
651
  return `z.enum([${nonNullValues.map(formatLiteral).join(", ")}])`;
436
652
  },
437
653
  ref(node) {
438
- if (!node.name) return void 0;
439
- const refName = node.ref ? ast.extractRefName(node.ref) ?? node.name : node.name;
440
- const resolvedName = node.ref ? this.options.resolver?.default(refName, "function") ?? refName : node.name;
654
+ if (!node.name) return null;
655
+ const refName = node.ref ? extractRefName(node.ref) ?? node.name : node.name;
656
+ const useInputVariant = node.ref != null && this.options.direction === "input" && containsCodec(node);
657
+ const resolvedName = node.ref ? useInputVariant ? this.options.resolver?.resolveInputSchemaName(refName) ?? refName : this.options.resolver?.default(refName, "function") ?? refName : node.name;
441
658
  if (node.ref && this.options.cyclicSchemas?.has(refName)) return `z.lazy(() => ${resolvedName})`;
442
659
  return resolvedName;
443
660
  },
444
661
  object(node) {
445
- let result = `z.object({\n ${node.properties.map((prop) => {
662
+ const objectBase = `z.object(${buildObject(node.properties.map((prop) => {
446
663
  const { name: propName, schema } = prop;
447
664
  const meta = ast.syncSchemaRef(schema);
448
665
  const isNullable = meta.nullable;
449
666
  const isOptional = schema.optional;
450
667
  const isNullish = schema.nullish;
451
668
  const hasSelfRef = this.options.cyclicSchemas != null && ast.containsCircularRef(schema, { circularSchemas: this.options.cyclicSchemas });
669
+ const savedCyclicSchemas = this.options.cyclicSchemas;
452
670
  if (hasSelfRef) this.options.cyclicSchemas = void 0;
453
671
  const baseOutput = this.transform(schema) ?? this.transform(ast.createSchema({ type: "unknown" }));
454
- if (hasSelfRef) this.options.cyclicSchemas = options.cyclicSchemas;
672
+ if (hasSelfRef) this.options.cyclicSchemas = savedCyclicSchemas;
455
673
  const wrappedOutput = this.options.wrapOutput ? this.options.wrapOutput({
456
674
  output: baseOutput,
457
675
  schema
458
676
  }) || baseOutput : baseOutput;
459
- let descriptionToApply = meta.description;
460
- if (schema.type !== "ref" && meta.type === "ref") descriptionToApply = void 0;
677
+ const descriptionToApply = schema.type !== "ref" && meta.type === "ref" ? void 0 : meta.description;
461
678
  const value = applyModifiers({
462
679
  value: wrappedOutput,
463
680
  nullable: isNullable,
@@ -466,78 +683,65 @@ const printerZod = ast.definePrinter((options) => {
466
683
  defaultValue: meta.default,
467
684
  description: descriptionToApply
468
685
  });
469
- if (hasSelfRef) return `get "${propName}"() { return ${value} }`;
470
- return `"${propName}": ${value}`;
471
- }).join(",\n ")}\n })`;
472
- if (node.additionalProperties && node.additionalProperties !== true) {
473
- const catchallType = this.transform(node.additionalProperties);
474
- if (catchallType) result += `.catchall(${catchallType})`;
475
- } else if (node.additionalProperties === true) result += `.catchall(${this.transform(ast.createSchema({ type: "unknown" }))})`;
476
- else if (node.additionalProperties === false) result += ".strict()";
477
- return result;
686
+ if (hasSelfRef) return `get ${objectKey(propName)}() { return ${value} }`;
687
+ return `${objectKey(propName)}: ${value}`;
688
+ }))})`;
689
+ return (() => {
690
+ if (node.additionalProperties && node.additionalProperties !== true) {
691
+ const catchallType = this.transform(node.additionalProperties);
692
+ return catchallType ? `${objectBase}.catchall(${catchallType})` : objectBase;
693
+ }
694
+ if (node.additionalProperties === true) return `${objectBase}.catchall(${this.transform(ast.createSchema({ type: "unknown" }))})`;
695
+ if (node.additionalProperties === false) return `${objectBase}.strict()`;
696
+ return objectBase;
697
+ })();
478
698
  },
479
699
  array(node) {
480
- let result = `z.array(${(node.items ?? []).map((item) => this.transform(item)).filter(Boolean).join(", ") || this.transform(ast.createSchema({ type: "unknown" }))})${lengthConstraints(node)}`;
481
- if (node.unique) result += `.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })`;
482
- return result;
700
+ const base = `z.array(${(node.items ?? []).map((item) => this.transform(item)).filter(Boolean).join(", ") || this.transform(ast.createSchema({ type: "unknown" }))})${lengthConstraints(node)}`;
701
+ return node.unique ? `${base}.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })` : base;
483
702
  },
484
703
  tuple(node) {
485
- return `z.tuple([${(node.items ?? []).map((item) => this.transform(item)).filter(Boolean).join(", ")}])`;
704
+ return `z.tuple(${buildList((node.items ?? []).map((item) => this.transform(item)).filter(Boolean))})`;
486
705
  },
487
706
  union(node) {
488
707
  const nodeMembers = node.members ?? [];
489
- const members = nodeMembers.map((m) => this.transform(m)).filter(Boolean);
708
+ const members = nodeMembers.map((memberNode) => {
709
+ const member = this.transform(memberNode);
710
+ return member && node.strategy === "one" ? strictOneOfMember$1(member, memberNode) : member;
711
+ }).filter(Boolean);
490
712
  if (members.length === 0) return "";
491
713
  if (members.length === 1) return members[0];
492
- if (node.discriminatorPropertyName && !nodeMembers.some((m) => m.type === "intersection")) return `z.discriminatedUnion(${stringify(node.discriminatorPropertyName)}, [${members.join(", ")}])`;
493
- return `z.union([${members.join(", ")}])`;
714
+ if (node.discriminatorPropertyName && !nodeMembers.some((m) => m.type === "intersection")) return `z.discriminatedUnion(${stringify(node.discriminatorPropertyName)}, ${buildList(members)})`;
715
+ return `z.union(${buildList(members)})`;
494
716
  },
495
717
  intersection(node) {
496
718
  const members = node.members ?? [];
497
719
  if (members.length === 0) return "";
498
720
  const [first, ...rest] = members;
499
721
  if (!first) return "";
500
- let base = this.transform(first);
501
- if (!base) return "";
502
- for (const member of rest) {
503
- if (member.primitive === "string") {
504
- const c = lengthConstraints(ast.narrowSchema(member, "string") ?? {});
505
- if (c) {
506
- base += c;
507
- continue;
508
- }
509
- } else if (member.primitive === "number" || member.primitive === "integer") {
510
- const c = numberConstraints(ast.narrowSchema(member, "number") ?? ast.narrowSchema(member, "integer") ?? {});
511
- if (c) {
512
- base += c;
513
- continue;
514
- }
515
- } else if (member.primitive === "array") {
516
- const c = lengthConstraints(ast.narrowSchema(member, "array") ?? {});
517
- if (c) {
518
- base += c;
519
- continue;
520
- }
521
- }
722
+ const firstBase = this.transform(first);
723
+ if (!firstBase) return "";
724
+ return rest.reduce((acc, member) => {
725
+ const constraint = getMemberConstraint(member);
726
+ if (constraint) return acc + constraint;
522
727
  const transformed = this.transform(member);
523
- if (transformed) base = `${base}.and(${transformed})`;
524
- }
525
- return base;
728
+ return transformed ? `${acc}.and(${transformed})` : acc;
729
+ }, firstBase);
526
730
  },
527
731
  ...options.nodes
528
732
  },
529
733
  print(node) {
530
734
  const { keysToOmit } = this.options;
531
- let base = this.transform(node);
532
- if (!base) return null;
735
+ const transformed = this.transform(node);
736
+ if (!transformed) return null;
533
737
  const meta = ast.syncSchemaRef(node);
534
- if (keysToOmit?.length && meta.primitive === "object" && !(meta.type === "union" && meta.discriminatorPropertyName)) {
535
- const lazyMatch = base.match(/^z\.lazy\(\(\)\s*=>\s*(.+)\)$/);
536
- if (lazyMatch) base = `z.lazy(() => ${lazyMatch[1]}.omit({ ${keysToOmit.map((k) => `"${k}": true`).join(", ")} }))`;
537
- else base = `${base}.omit({ ${keysToOmit.map((k) => `"${k}": true`).join(", ")} })`;
538
- }
539
738
  return applyModifiers({
540
- value: base,
739
+ value: (() => {
740
+ if (!keysToOmit?.length || meta.primitive !== "object" || meta.type === "union" && meta.discriminatorPropertyName) return transformed;
741
+ const lazyMatch = transformed.match(/^z\.lazy\(\(\)\s*=>\s*(.+)\)$/);
742
+ if (lazyMatch) return `z.lazy(() => ${lazyMatch[1]}.omit({ ${keysToOmit.map((k) => `"${k}": true`).join(", ")} }))`;
743
+ return `${transformed}.omit({ ${keysToOmit.map((k) => `"${k}": true`).join(", ")} })`;
744
+ })(),
541
745
  nullable: meta.nullable,
542
746
  optional: meta.optional,
543
747
  nullish: meta.nullish,
@@ -549,6 +753,15 @@ const printerZod = ast.definePrinter((options) => {
549
753
  });
550
754
  //#endregion
551
755
  //#region src/printers/printerZodMini.ts
756
+ function strictOneOfMember(member, node) {
757
+ if (node.type === "object" && (node.additionalProperties === void 0 || node.additionalProperties === false)) return member.replace(/^z\.object\(/, "z.strictObject(");
758
+ return member;
759
+ }
760
+ function getMemberConstraintMini(member) {
761
+ if (member.primitive === "string") return lengthChecksMini(ast.narrowSchema(member, "string") ?? {}) || void 0;
762
+ if (member.primitive === "number" || member.primitive === "integer") return numberChecksMini(ast.narrowSchema(member, "number") ?? ast.narrowSchema(member, "integer") ?? {}) || void 0;
763
+ if (member.primitive === "array") return lengthChecksMini(ast.narrowSchema(member, "array") ?? {}) || void 0;
764
+ }
552
765
  /**
553
766
  * Zod v4 **Mini** printer built with `definePrinter`.
554
767
  *
@@ -617,23 +830,24 @@ const printerZodMini = ast.definePrinter((options) => {
617
830
  return `z.enum([${nonNullValues.map(formatLiteral).join(", ")}])`;
618
831
  },
619
832
  ref(node) {
620
- if (!node.name) return void 0;
621
- const refName = node.ref ? ast.extractRefName(node.ref) ?? node.name : node.name;
833
+ if (!node.name) return null;
834
+ const refName = node.ref ? extractRefName(node.ref) ?? node.name : node.name;
622
835
  const resolvedName = node.ref ? this.options.resolver?.default(refName, "function") ?? refName : node.name;
623
836
  if (node.ref && this.options.cyclicSchemas?.has(refName)) return `z.lazy(() => ${resolvedName})`;
624
837
  return resolvedName;
625
838
  },
626
839
  object(node) {
627
- return `z.object({\n ${node.properties.map((prop) => {
840
+ return `z.object(${buildObject(node.properties.map((prop) => {
628
841
  const { name: propName, schema } = prop;
629
842
  const meta = ast.syncSchemaRef(schema);
630
843
  const isNullable = meta.nullable;
631
844
  const isOptional = schema.optional;
632
845
  const isNullish = schema.nullish;
633
846
  const hasSelfRef = this.options.cyclicSchemas != null && ast.containsCircularRef(schema, { circularSchemas: this.options.cyclicSchemas });
847
+ const savedCyclicSchemas = this.options.cyclicSchemas;
634
848
  if (hasSelfRef) this.options.cyclicSchemas = void 0;
635
849
  const baseOutput = this.transform(schema) ?? this.transform(ast.createSchema({ type: "unknown" }));
636
- if (hasSelfRef) this.options.cyclicSchemas = options.cyclicSchemas;
850
+ if (hasSelfRef) this.options.cyclicSchemas = savedCyclicSchemas;
637
851
  const value = applyMiniModifiers({
638
852
  value: this.options.wrapOutput ? this.options.wrapOutput({
639
853
  output: baseOutput,
@@ -644,72 +858,56 @@ const printerZodMini = ast.definePrinter((options) => {
644
858
  nullish: isNullish,
645
859
  defaultValue: meta.default
646
860
  });
647
- if (hasSelfRef) return `get "${propName}"() { return ${value} }`;
648
- return `"${propName}": ${value}`;
649
- }).join(",\n ")}\n })`;
861
+ if (hasSelfRef) return `get ${objectKey(propName)}() { return ${value} }`;
862
+ return `${objectKey(propName)}: ${value}`;
863
+ }))})`;
650
864
  },
651
865
  array(node) {
652
- let result = `z.array(${(node.items ?? []).map((item) => this.transform(item)).filter(Boolean).join(", ") || this.transform(ast.createSchema({ type: "unknown" }))})${lengthChecksMini(node)}`;
653
- if (node.unique) result += `.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })`;
654
- return result;
866
+ const base = `z.array(${(node.items ?? []).map((item) => this.transform(item)).filter(Boolean).join(", ") || this.transform(ast.createSchema({ type: "unknown" }))})${lengthChecksMini(node)}`;
867
+ return node.unique ? `${base}.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })` : base;
655
868
  },
656
869
  tuple(node) {
657
- return `z.tuple([${(node.items ?? []).map((item) => this.transform(item)).filter(Boolean).join(", ")}])`;
870
+ return `z.tuple(${buildList((node.items ?? []).map((item) => this.transform(item)).filter(Boolean))})`;
658
871
  },
659
872
  union(node) {
660
873
  const nodeMembers = node.members ?? [];
661
- const members = nodeMembers.map((m) => this.transform(m)).filter(Boolean);
874
+ const members = nodeMembers.map((memberNode) => {
875
+ const member = this.transform(memberNode);
876
+ return member && node.strategy === "one" ? strictOneOfMember(member, memberNode) : member;
877
+ }).filter(Boolean);
662
878
  if (members.length === 0) return "";
663
879
  if (members.length === 1) return members[0];
664
- if (node.discriminatorPropertyName && !nodeMembers.some((m) => m.type === "intersection")) return `z.discriminatedUnion(${stringify(node.discriminatorPropertyName)}, [${members.join(", ")}])`;
665
- return `z.union([${members.join(", ")}])`;
880
+ if (node.discriminatorPropertyName && !nodeMembers.some((m) => m.type === "intersection")) return `z.discriminatedUnion(${stringify(node.discriminatorPropertyName)}, ${buildList(members)})`;
881
+ return `z.union(${buildList(members)})`;
666
882
  },
667
883
  intersection(node) {
668
884
  const members = node.members ?? [];
669
885
  if (members.length === 0) return "";
670
886
  const [first, ...rest] = members;
671
887
  if (!first) return "";
672
- let base = this.transform(first);
673
- if (!base) return "";
674
- for (const member of rest) {
675
- if (member.primitive === "string") {
676
- const c = lengthChecksMini(ast.narrowSchema(member, "string") ?? {});
677
- if (c) {
678
- base += c;
679
- continue;
680
- }
681
- } else if (member.primitive === "number" || member.primitive === "integer") {
682
- const c = numberChecksMini(ast.narrowSchema(member, "number") ?? ast.narrowSchema(member, "integer") ?? {});
683
- if (c) {
684
- base += c;
685
- continue;
686
- }
687
- } else if (member.primitive === "array") {
688
- const c = lengthChecksMini(ast.narrowSchema(member, "array") ?? {});
689
- if (c) {
690
- base += c;
691
- continue;
692
- }
693
- }
888
+ const firstBase = this.transform(first);
889
+ if (!firstBase) return "";
890
+ return rest.reduce((acc, member) => {
891
+ const constraint = getMemberConstraintMini(member);
892
+ if (constraint) return acc + constraint;
694
893
  const transformed = this.transform(member);
695
- if (transformed) base = `z.intersection(${base}, ${transformed})`;
696
- }
697
- return base;
894
+ return transformed ? `z.intersection(${acc}, ${transformed})` : acc;
895
+ }, firstBase);
698
896
  },
699
897
  ...options.nodes
700
898
  },
701
899
  print(node) {
702
900
  const { keysToOmit } = this.options;
703
- let base = this.transform(node);
704
- if (!base) return null;
901
+ const transformed = this.transform(node);
902
+ if (!transformed) return null;
705
903
  const meta = ast.syncSchemaRef(node);
706
- if (keysToOmit?.length && meta.primitive === "object" && !(meta.type === "union" && meta.discriminatorPropertyName)) {
707
- const lazyMatch = base.match(/^z\.lazy\(\(\)\s*=>\s*(.+)\)$/);
708
- if (lazyMatch) base = `z.lazy(() => ${lazyMatch[1]}.omit({ ${keysToOmit.map((k) => `"${k}": true`).join(", ")} }))`;
709
- else base = `${base}.omit({ ${keysToOmit.map((k) => `"${k}": true`).join(", ")} })`;
710
- }
711
904
  return applyMiniModifiers({
712
- value: base,
905
+ value: (() => {
906
+ if (!keysToOmit?.length || meta.primitive !== "object" || meta.type === "union" && meta.discriminatorPropertyName) return transformed;
907
+ const lazyMatch = transformed.match(/^z\.lazy\(\(\)\s*=>\s*(.+)\)$/);
908
+ if (lazyMatch) return `z.lazy(() => ${lazyMatch[1]}.omit({ ${keysToOmit.map((k) => `"${k}": true`).join(", ")} }))`;
909
+ return `${transformed}.omit({ ${keysToOmit.map((k) => `"${k}": true`).join(", ")} })`;
910
+ })(),
713
911
  nullable: meta.nullable,
714
912
  optional: meta.optional,
715
913
  nullish: meta.nullish,
@@ -720,6 +918,62 @@ const printerZodMini = ast.definePrinter((options) => {
720
918
  });
721
919
  //#endregion
722
920
  //#region src/generators/zodGenerator.tsx
921
+ const zodPrinterCache = /* @__PURE__ */ new WeakMap();
922
+ const zodMiniPrinterCache = /* @__PURE__ */ new WeakMap();
923
+ /**
924
+ * Returns the cached `output`/`input` direction printers for a resolver, building them on
925
+ * first use. The `input` printer encodes `Date → string` for request bodies; `output` decodes
926
+ * `string → Date` for responses. Schemas without `dateType: 'date'` fields print identically.
927
+ */
928
+ function getStdPrinters(resolver, params) {
929
+ const cached = zodPrinterCache.get(resolver);
930
+ if (cached && cached.coercion === params.coercion && cached.guidType === params.guidType && cached.dateType === params.dateType) return {
931
+ output: cached.output,
932
+ input: cached.input
933
+ };
934
+ const base = {
935
+ ...params,
936
+ resolver
937
+ };
938
+ const output = printerZod({
939
+ ...base,
940
+ direction: "output"
941
+ });
942
+ const input = printerZod({
943
+ ...base,
944
+ direction: "input"
945
+ });
946
+ zodPrinterCache.set(resolver, {
947
+ output,
948
+ input,
949
+ coercion: params.coercion,
950
+ guidType: params.guidType,
951
+ dateType: params.dateType
952
+ });
953
+ return {
954
+ output,
955
+ input
956
+ };
957
+ }
958
+ function getMiniPrinter(resolver, params) {
959
+ const cached = zodMiniPrinterCache.get(resolver);
960
+ if (cached && cached.guidType === params.guidType) return cached.printer;
961
+ const p = printerZodMini({
962
+ ...params,
963
+ resolver
964
+ });
965
+ zodMiniPrinterCache.set(resolver, {
966
+ printer: p,
967
+ guidType: params.guidType
968
+ });
969
+ return p;
970
+ }
971
+ /**
972
+ * Built-in generator for `@kubb/plugin-zod`. Emits one Zod schema per
973
+ * schema in the spec plus per-operation request/response/parameter schemas.
974
+ * When `mini: true`, schemas use the Zod Mini functional API instead of
975
+ * chainable methods.
976
+ */
723
977
  const zodGenerator = defineGenerator({
724
978
  name: "zod",
725
979
  renderer: jsxRenderer,
@@ -728,9 +982,11 @@ const zodGenerator = defineGenerator({
728
982
  const { output, coercion, guidType, mini, wrapOutput, inferred, importPath, group, printer } = ctx.options;
729
983
  const dateType = adapter.options.dateType;
730
984
  if (!node.name) return;
731
- const mode = ctx.getMode(output);
732
985
  const isZodImport = ZOD_NAMESPACE_IMPORTS.has(importPath);
733
- const imports = adapter.getImports(node, (schemaName) => ({
986
+ const cyclicSchemas = new Set(ctx.meta.circularNames);
987
+ const hasCodec = !mini && containsCodec(node);
988
+ const codecRefNames = new Set(hasCodec ? ast.collect(node, { schema: (n) => n.type === "ref" && n.ref && containsCodec(n) ? extractRefName(n.ref) ?? void 0 : void 0 }) : []);
989
+ const importEntries = adapter.getImports(node, (schemaName) => ({
734
990
  name: resolver.resolveSchemaName(schemaName),
735
991
  path: resolver.resolveFile({
736
992
  name: schemaName,
@@ -738,9 +994,27 @@ const zodGenerator = defineGenerator({
738
994
  }, {
739
995
  root,
740
996
  output,
741
- group
997
+ group: group ?? void 0
742
998
  }).path
743
999
  }));
1000
+ const inputImportEntries = hasCodec ? [...codecRefNames].map((schemaName) => ({
1001
+ name: [resolver.resolveInputSchemaName(schemaName)],
1002
+ path: resolver.resolveFile({
1003
+ name: schemaName,
1004
+ extname: ".ts"
1005
+ }, {
1006
+ root,
1007
+ output,
1008
+ group: group ?? void 0
1009
+ }).path
1010
+ })) : [];
1011
+ const seenImports = /* @__PURE__ */ new Set();
1012
+ const imports = [...importEntries, ...inputImportEntries].filter((imp) => {
1013
+ const key = `${Array.isArray(imp.name) ? imp.name.join(",") : imp.name}|${imp.path}`;
1014
+ if (seenImports.has(key)) return false;
1015
+ seenImports.add(key);
1016
+ return true;
1017
+ });
744
1018
  const meta = {
745
1019
  name: resolver.resolveSchemaName(node.name),
746
1020
  file: resolver.resolveFile({
@@ -749,37 +1023,43 @@ const zodGenerator = defineGenerator({
749
1023
  }, {
750
1024
  root,
751
1025
  output,
752
- group
1026
+ group: group ?? void 0
753
1027
  })
754
1028
  };
755
- const inferTypeName = inferred ? resolver.resolveSchemaTypeName(node.name) : void 0;
756
- const cyclicSchemas = adapter.inputNode ? ast.findCircularSchemas(adapter.inputNode.schemas) : void 0;
757
- const schemaPrinter = mini ? printerZodMini({
1029
+ const inferTypeName = inferred ? resolver.resolveSchemaTypeName(node.name) : null;
1030
+ const stdPrinters = mini ? null : getStdPrinters(resolver, {
1031
+ coercion,
758
1032
  guidType,
1033
+ dateType,
759
1034
  wrapOutput,
760
- resolver,
761
1035
  cyclicSchemas,
762
1036
  nodes: printer?.nodes
763
- }) : printerZod({
764
- coercion,
1037
+ });
1038
+ const schemaPrinter = mini ? getMiniPrinter(resolver, {
765
1039
  guidType,
766
- dateType,
767
1040
  wrapOutput,
768
- resolver,
769
1041
  cyclicSchemas,
770
1042
  nodes: printer?.nodes
771
- });
1043
+ }) : stdPrinters.output;
772
1044
  return /* @__PURE__ */ jsxs(File, {
773
1045
  baseName: meta.file.baseName,
774
1046
  path: meta.file.path,
775
1047
  meta: meta.file.meta,
776
- banner: resolver.resolveBanner(adapter.inputNode, {
1048
+ banner: resolver.resolveBanner(ctx.meta, {
777
1049
  output,
778
- config
1050
+ config,
1051
+ file: {
1052
+ path: meta.file.path,
1053
+ baseName: meta.file.baseName
1054
+ }
779
1055
  }),
780
- footer: resolver.resolveFooter(adapter.inputNode, {
1056
+ footer: resolver.resolveFooter(ctx.meta, {
781
1057
  output,
782
- config
1058
+ config,
1059
+ file: {
1060
+ path: meta.file.path,
1061
+ baseName: meta.file.baseName
1062
+ }
783
1063
  }),
784
1064
  children: [
785
1065
  /* @__PURE__ */ jsx(File.Import, {
@@ -787,25 +1067,35 @@ const zodGenerator = defineGenerator({
787
1067
  path: importPath,
788
1068
  isNameSpace: isZodImport
789
1069
  }),
790
- mode === "split" && imports.map((imp) => /* @__PURE__ */ jsx(File.Import, {
1070
+ imports.map((imp) => /* @__PURE__ */ jsx(File.Import, {
791
1071
  root: meta.file.path,
792
1072
  path: imp.path,
793
1073
  name: imp.name
794
- }, [node.name, imp.path].join("-"))),
1074
+ }, [
1075
+ node.name,
1076
+ imp.path,
1077
+ imp.name
1078
+ ].join("-"))),
795
1079
  /* @__PURE__ */ jsx(Zod, {
796
1080
  name: meta.name,
797
1081
  node,
798
1082
  printer: schemaPrinter,
799
1083
  inferTypeName
1084
+ }),
1085
+ hasCodec && stdPrinters && /* @__PURE__ */ jsx(Zod, {
1086
+ name: resolver.resolveInputSchemaName(node.name),
1087
+ node,
1088
+ printer: stdPrinters.input,
1089
+ inferTypeName: inferred ? resolver.resolveInputSchemaTypeName(node.name) : null
800
1090
  })
801
1091
  ]
802
1092
  });
803
1093
  },
804
1094
  operation(node, ctx) {
1095
+ if (!ast.isHttpOperationNode(node)) return null;
805
1096
  const { adapter, config, resolver, root } = ctx;
806
1097
  const { output, coercion, guidType, mini, wrapOutput, inferred, importPath, group, paramsCasing, printer } = ctx.options;
807
1098
  const dateType = adapter.options.dateType;
808
- const mode = ctx.getMode(output);
809
1099
  const isZodImport = ZOD_NAMESPACE_IMPORTS.has(importPath);
810
1100
  const params = ast.caseParams(node.parameters, paramsCasing);
811
1101
  const meta = { file: resolver.resolveFile({
@@ -816,31 +1106,37 @@ const zodGenerator = defineGenerator({
816
1106
  }, {
817
1107
  root,
818
1108
  output,
819
- group
1109
+ group: group ?? void 0
820
1110
  }) };
821
- const cyclicSchemas = adapter.inputNode ? ast.findCircularSchemas(adapter.inputNode.schemas) : void 0;
822
- function renderSchemaEntry({ schema, name, keysToOmit }) {
1111
+ const cyclicSchemas = new Set(ctx.meta.circularNames);
1112
+ function renderSchemaEntry({ schema, name, keysToOmit, direction = "output" }) {
823
1113
  if (!schema) return null;
824
- const inferTypeName = inferred ? resolver.resolveTypeName(name) : void 0;
1114
+ const inferTypeName = inferred ? resolver.resolveTypeName(name) : null;
1115
+ const codecRefNames = direction === "input" && !mini ? new Set(ast.collect(schema, { schema: (n) => n.type === "ref" && n.ref && containsCodec(n) ? extractRefName(n.ref) ?? void 0 : void 0 })) : null;
825
1116
  const imports = adapter.getImports(schema, (schemaName) => ({
826
- name: resolver.resolveSchemaName(schemaName),
1117
+ name: codecRefNames?.has(schemaName) ? resolver.resolveInputSchemaName(schemaName) : resolver.resolveSchemaName(schemaName),
827
1118
  path: resolver.resolveFile({
828
1119
  name: schemaName,
829
1120
  extname: ".ts"
830
1121
  }, {
831
1122
  root,
832
1123
  output,
833
- group
1124
+ group: group ?? void 0
834
1125
  }).path
835
1126
  }));
836
- const schemaPrinter = mini ? printerZodMini({
1127
+ const schemaPrinter = mini ? keysToOmit?.length ? printerZodMini({
837
1128
  guidType,
838
1129
  wrapOutput,
839
1130
  resolver,
840
1131
  keysToOmit,
841
1132
  cyclicSchemas,
842
1133
  nodes: printer?.nodes
843
- }) : printerZod({
1134
+ }) : getMiniPrinter(resolver, {
1135
+ guidType,
1136
+ wrapOutput,
1137
+ cyclicSchemas,
1138
+ nodes: printer?.nodes
1139
+ }) : keysToOmit?.length ? printerZod({
844
1140
  coercion,
845
1141
  guidType,
846
1142
  dateType,
@@ -848,9 +1144,17 @@ const zodGenerator = defineGenerator({
848
1144
  resolver,
849
1145
  keysToOmit,
850
1146
  cyclicSchemas,
1147
+ nodes: printer?.nodes,
1148
+ direction
1149
+ }) : getStdPrinters(resolver, {
1150
+ coercion,
1151
+ guidType,
1152
+ dateType,
1153
+ wrapOutput,
1154
+ cyclicSchemas,
851
1155
  nodes: printer?.nodes
852
- });
853
- return /* @__PURE__ */ jsxs(Fragment, { children: [mode === "split" && imports.map((imp) => /* @__PURE__ */ jsx(File.Import, {
1156
+ })[direction];
1157
+ return /* @__PURE__ */ jsxs(Fragment, { children: [imports.map((imp) => /* @__PURE__ */ jsx(File.Import, {
854
1158
  root: meta.file.path,
855
1159
  path: imp.path,
856
1160
  name: imp.name
@@ -865,22 +1169,48 @@ const zodGenerator = defineGenerator({
865
1169
  inferTypeName
866
1170
  })] });
867
1171
  }
1172
+ function buildContentTypeVariants(entries, baseName, decorate, direction) {
1173
+ const variants = resolveContentTypeVariants(entries, baseName);
1174
+ const unionSchema = ast.createSchema({
1175
+ type: "union",
1176
+ members: variants.map((variant) => ast.createSchema({
1177
+ type: "ref",
1178
+ name: variant.name
1179
+ }))
1180
+ });
1181
+ return /* @__PURE__ */ jsxs(Fragment, { children: [variants.map((variant) => renderSchemaEntry({
1182
+ schema: decorate ? decorate(variant.schema) : variant.schema,
1183
+ name: variant.name,
1184
+ keysToOmit: variant.keysToOmit,
1185
+ direction
1186
+ })), renderSchemaEntry({
1187
+ schema: unionSchema,
1188
+ name: baseName,
1189
+ direction
1190
+ })] });
1191
+ }
868
1192
  const paramSchemas = params.map((param) => renderSchemaEntry({
869
1193
  schema: param.schema,
870
- name: resolver.resolveParamName(node, param)
1194
+ name: resolver.resolveParamName(node, param),
1195
+ direction: "input"
871
1196
  }));
872
- const responseSchemas = node.responses.map((res) => renderSchemaEntry({
873
- schema: res.schema,
874
- name: resolver.resolveResponseStatusName(node, res.statusCode),
875
- keysToOmit: res.keysToOmit
876
- }));
877
- const responsesWithSchema = node.responses.filter((res) => res.schema);
1197
+ const responseSchemas = node.responses.map((res) => {
1198
+ const variants = (res.content ?? []).filter((entry) => entry.schema);
1199
+ if (variants.length > 1) return buildContentTypeVariants(res.content, resolver.resolveResponseStatusName(node, res.statusCode));
1200
+ const primary = variants[0] ?? res.content?.[0];
1201
+ return renderSchemaEntry({
1202
+ schema: primary?.schema ?? null,
1203
+ name: resolver.resolveResponseStatusName(node, res.statusCode),
1204
+ keysToOmit: primary?.keysToOmit
1205
+ });
1206
+ });
1207
+ const responsesWithSchema = node.responses.filter((res) => res.content?.some((entry) => entry.schema));
878
1208
  const responseUnionSchema = responsesWithSchema.length > 0 ? (() => {
879
1209
  const responseUnionName = resolver.resolveResponseName(node);
880
- if (new Set(responsesWithSchema.flatMap((res) => res.schema ? adapter.getImports(res.schema, (schemaName) => ({
1210
+ if (new Set(responsesWithSchema.flatMap((res) => (res.content ?? []).flatMap((entry) => entry.schema ? adapter.getImports(entry.schema, (schemaName) => ({
881
1211
  name: resolver.resolveSchemaName(schemaName),
882
1212
  path: ""
883
- })).flatMap((imp) => Array.isArray(imp.name) ? imp.name : [imp.name]) : [])).has(responseUnionName)) return null;
1213
+ })).flatMap((imp) => Array.isArray(imp.name) ? imp.name : [imp.name]) : []))).has(responseUnionName)) return null;
884
1214
  const members = responsesWithSchema.map((res) => ast.createSchema({
885
1215
  type: "ref",
886
1216
  name: resolver.resolveResponseStatusName(node, res.statusCode)
@@ -893,25 +1223,46 @@ const zodGenerator = defineGenerator({
893
1223
  name: responseUnionName
894
1224
  });
895
1225
  })() : null;
896
- const requestSchema = node.requestBody?.content?.[0]?.schema ? renderSchemaEntry({
897
- schema: {
898
- ...node.requestBody.content[0].schema,
899
- description: node.requestBody.description ?? node.requestBody.content[0].schema.description
900
- },
901
- name: resolver.resolveDataName(node),
902
- keysToOmit: node.requestBody.content[0].keysToOmit
903
- }) : null;
1226
+ const requestBodyContent = node.requestBody?.content ?? [];
1227
+ const requestSchema = (() => {
1228
+ if (requestBodyContent.length === 0) return null;
1229
+ if (requestBodyContent.length === 1) {
1230
+ const entry = requestBodyContent[0];
1231
+ if (!entry.schema) return null;
1232
+ return renderSchemaEntry({
1233
+ schema: {
1234
+ ...entry.schema,
1235
+ description: node.requestBody.description ?? entry.schema.description
1236
+ },
1237
+ name: resolver.resolveDataName(node),
1238
+ keysToOmit: entry.keysToOmit,
1239
+ direction: "input"
1240
+ });
1241
+ }
1242
+ return buildContentTypeVariants(requestBodyContent, resolver.resolveDataName(node), (schema) => ({
1243
+ ...schema,
1244
+ description: node.requestBody.description ?? schema.description
1245
+ }), "input");
1246
+ })();
904
1247
  return /* @__PURE__ */ jsxs(File, {
905
1248
  baseName: meta.file.baseName,
906
1249
  path: meta.file.path,
907
1250
  meta: meta.file.meta,
908
- banner: resolver.resolveBanner(adapter.inputNode, {
1251
+ banner: resolver.resolveBanner(ctx.meta, {
909
1252
  output,
910
- config
1253
+ config,
1254
+ file: {
1255
+ path: meta.file.path,
1256
+ baseName: meta.file.baseName
1257
+ }
911
1258
  }),
912
- footer: resolver.resolveFooter(adapter.inputNode, {
1259
+ footer: resolver.resolveFooter(ctx.meta, {
913
1260
  output,
914
- config
1261
+ config,
1262
+ file: {
1263
+ path: meta.file.path,
1264
+ baseName: meta.file.baseName
1265
+ }
915
1266
  }),
916
1267
  children: [
917
1268
  /* @__PURE__ */ jsx(File.Import, {
@@ -927,7 +1278,7 @@ const zodGenerator = defineGenerator({
927
1278
  });
928
1279
  },
929
1280
  operations(nodes, ctx) {
930
- const { adapter, config, resolver, root } = ctx;
1281
+ const { config, resolver, root } = ctx;
931
1282
  const { output, importPath, group, operations, paramsCasing } = ctx.options;
932
1283
  if (!operations) return;
933
1284
  const isZodImport = ZOD_NAMESPACE_IMPORTS.has(importPath);
@@ -937,9 +1288,9 @@ const zodGenerator = defineGenerator({
937
1288
  }, {
938
1289
  root,
939
1290
  output,
940
- group
1291
+ group: group ?? void 0
941
1292
  }) };
942
- const transformedOperations = nodes.map((node) => {
1293
+ const transformedOperations = nodes.filter(ast.isHttpOperationNode).map((node) => {
943
1294
  return {
944
1295
  node,
945
1296
  data: buildSchemaNames(node, {
@@ -962,7 +1313,7 @@ const zodGenerator = defineGenerator({
962
1313
  }, {
963
1314
  root,
964
1315
  output,
965
- group
1316
+ group: group ?? void 0
966
1317
  });
967
1318
  return names.map((name) => /* @__PURE__ */ jsx(File.Import, {
968
1319
  name: [name],
@@ -974,13 +1325,21 @@ const zodGenerator = defineGenerator({
974
1325
  baseName: meta.file.baseName,
975
1326
  path: meta.file.path,
976
1327
  meta: meta.file.meta,
977
- banner: resolver.resolveBanner(adapter.inputNode, {
1328
+ banner: resolver.resolveBanner(ctx.meta, {
978
1329
  output,
979
- config
1330
+ config,
1331
+ file: {
1332
+ path: meta.file.path,
1333
+ baseName: meta.file.baseName
1334
+ }
980
1335
  }),
981
- footer: resolver.resolveFooter(adapter.inputNode, {
1336
+ footer: resolver.resolveFooter(ctx.meta, {
982
1337
  output,
983
- config
1338
+ config,
1339
+ file: {
1340
+ path: meta.file.path,
1341
+ baseName: meta.file.baseName
1342
+ }
984
1343
  }),
985
1344
  children: [
986
1345
  /* @__PURE__ */ jsx(File.Import, {
@@ -1001,92 +1360,110 @@ const zodGenerator = defineGenerator({
1001
1360
  //#endregion
1002
1361
  //#region src/resolvers/resolverZod.ts
1003
1362
  /**
1004
- * Naming convention resolver for Zod plugin.
1363
+ * Default resolver used by `@kubb/plugin-zod`. Decides the names and file
1364
+ * paths for every generated Zod schema. Schemas use camelCase with a
1365
+ * `Schema` suffix (`listPetsSchema`); their inferred types use PascalCase
1366
+ * with a `SchemaType` suffix (`PetSchemaType`), so the value and the type
1367
+ * never share an identifier even when the schema name is all-uppercase.
1005
1368
  *
1006
- * Provides default naming helpers using camelCase with a `Schema` suffix for schemas.
1369
+ * @example Resolve schema and type names
1370
+ * ```ts
1371
+ * import { resolverZod } from '@kubb/plugin-zod'
1007
1372
  *
1008
- * @example
1009
- * `resolverZod.default('list pets', 'function') // 'listPetsSchema'`
1373
+ * resolverZod.default('list pets', 'function') // 'listPetsSchema'
1374
+ * resolverZod.resolveSchemaTypeName('pet') // 'PetSchemaType'
1375
+ * ```
1010
1376
  */
1011
- const resolverZod = defineResolver((ctx) => {
1377
+ const resolverZod = defineResolver(() => {
1012
1378
  return {
1013
1379
  name: "default",
1014
1380
  pluginName: "plugin-zod",
1015
1381
  default(name, type) {
1016
- return camelCase(name, {
1017
- isFile: type === "file",
1018
- suffix: type ? "schema" : void 0
1019
- });
1382
+ if (type === "file") return toFilePath(name, (part) => camelCase(part, { suffix: "schema" }));
1383
+ return ensureValidVarName(camelCase(name, { suffix: type ? "schema" : void 0 }));
1020
1384
  },
1021
1385
  resolveSchemaName(name) {
1022
- return camelCase(name, { suffix: "schema" });
1386
+ return ensureValidVarName(camelCase(name, { suffix: "schema" }));
1023
1387
  },
1024
1388
  resolveSchemaTypeName(name) {
1025
- return pascalCase(name, { suffix: "schema" });
1389
+ return ensureValidVarName(pascalCase(name, { suffix: "schema type" }));
1390
+ },
1391
+ resolveInputSchemaName(name) {
1392
+ return this.resolveSchemaName(`${name} input`);
1393
+ },
1394
+ resolveInputSchemaTypeName(name) {
1395
+ return this.resolveSchemaTypeName(`${name} input`);
1026
1396
  },
1027
1397
  resolveTypeName(name) {
1028
- return pascalCase(name);
1398
+ return ensureValidVarName(pascalCase(name, { suffix: "type" }));
1029
1399
  },
1030
1400
  resolvePathName(name, type) {
1031
- return ctx.default(name, type);
1401
+ return this.default(name, type);
1032
1402
  },
1033
1403
  resolveParamName(node, param) {
1034
- return ctx.resolveSchemaName(`${node.operationId} ${param.in} ${param.name}`);
1404
+ return this.resolveSchemaName(`${node.operationId} ${param.in} ${param.name}`);
1035
1405
  },
1036
1406
  resolveResponseStatusName(node, statusCode) {
1037
- return ctx.resolveSchemaName(`${node.operationId} Status ${statusCode}`);
1407
+ return this.resolveSchemaName(`${node.operationId} Status ${statusCode}`);
1038
1408
  },
1039
1409
  resolveDataName(node) {
1040
- return ctx.resolveSchemaName(`${node.operationId} Data`);
1410
+ return this.resolveSchemaName(`${node.operationId} Data`);
1041
1411
  },
1042
1412
  resolveResponsesName(node) {
1043
- return ctx.resolveSchemaName(`${node.operationId} Responses`);
1413
+ return this.resolveSchemaName(`${node.operationId} Responses`);
1044
1414
  },
1045
1415
  resolveResponseName(node) {
1046
- return ctx.resolveSchemaName(`${node.operationId} Response`);
1416
+ return this.resolveSchemaName(`${node.operationId} Response`);
1047
1417
  },
1048
1418
  resolvePathParamsName(node, param) {
1049
- return ctx.resolveParamName(node, param);
1419
+ return this.resolveParamName(node, param);
1050
1420
  },
1051
1421
  resolveQueryParamsName(node, param) {
1052
- return ctx.resolveParamName(node, param);
1422
+ return this.resolveParamName(node, param);
1053
1423
  },
1054
1424
  resolveHeaderParamsName(node, param) {
1055
- return ctx.resolveParamName(node, param);
1425
+ return this.resolveParamName(node, param);
1056
1426
  }
1057
1427
  };
1058
1428
  });
1059
1429
  //#endregion
1060
1430
  //#region src/plugin.ts
1061
1431
  /**
1062
- * Canonical plugin name for `@kubb/plugin-zod`, used in driver lookups and warnings.
1432
+ * Canonical plugin name for `@kubb/plugin-zod`. Used for driver lookups and
1433
+ * cross-plugin dependency references.
1063
1434
  */
1064
1435
  const pluginZodName = "plugin-zod";
1065
1436
  /**
1066
- * Generates Zod validation schemas from an OpenAPI specification.
1067
- * Walks schemas and operations, delegates to generators, and writes barrel files
1068
- * based on the configured `barrelType`.
1437
+ * Generates Zod v4 schemas from an OpenAPI spec. Use them to validate API
1438
+ * responses at runtime, build form schemas, or feed back into router libraries
1439
+ * that consume Zod (tRPC, Hono, Elysia). Pair with `@kubb/plugin-client` and
1440
+ * set the client's `parser: 'zod'` to validate every response automatically.
1069
1441
  *
1070
- * @example Zod schema generator
1442
+ * @example
1071
1443
  * ```ts
1072
- * import pluginZod from '@kubb/plugin-zod'
1444
+ * import { defineConfig } from 'kubb'
1445
+ * import { pluginTs } from '@kubb/plugin-ts'
1446
+ * import { pluginZod } from '@kubb/plugin-zod'
1447
+ *
1073
1448
  * export default defineConfig({
1074
- * plugins: [pluginZod({ output: { path: 'zod' } })]
1449
+ * input: { path: './petStore.yaml' },
1450
+ * output: { path: './src/gen' },
1451
+ * plugins: [
1452
+ * pluginTs(),
1453
+ * pluginZod({
1454
+ * output: { path: './zod' },
1455
+ * typed: true,
1456
+ * }),
1457
+ * ],
1075
1458
  * })
1076
1459
  * ```
1077
1460
  */
1078
1461
  const pluginZod = definePlugin((options) => {
1079
1462
  const { output = {
1080
1463
  path: "zod",
1081
- barrelType: "named"
1464
+ barrel: { type: "named" }
1082
1465
  }, group, exclude = [], include, override = [], typed = false, operations = false, mini = false, guidType = "uuid", importPath = mini ? "zod/mini" : "zod", coercion = false, inferred = false, wrapOutput = void 0, paramsCasing, printer, resolver: userResolver, transformer: userTransformer, generators: userGenerators = [] } = options;
1083
- const groupConfig = group ? {
1084
- ...group,
1085
- name: (ctx) => {
1086
- if (group.type === "path") return `${ctx.group.split("/")[1]}`;
1087
- return `${camelCase(ctx.group)}Controller`;
1088
- }
1089
- } : void 0;
1466
+ const groupConfig = createGroupConfig(group);
1090
1467
  return {
1091
1468
  name: pluginZodName,
1092
1469
  options,