@kubb/plugin-zod 5.0.0-beta.10 → 5.0.0-beta.100

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,7 +1,169 @@
1
- import { t as __name } from "./chunk--u3MIqq1.js";
2
- import { ast, defineGenerator, definePlugin, defineResolver } from "@kubb/core";
3
- import { Const, File, Type, jsxRenderer } from "@kubb/renderer-jsx";
4
- import { Fragment, jsx, jsxs } from "@kubb/renderer-jsx/jsx-runtime";
1
+ import { t as __name } from "./rolldown-runtime-C0LytTxp.js";
2
+ import { Resolver, ast, containsCircularRef, createResolver, defineGenerator, definePlugin, extractRefName, syncSchemaRef } from "kubb/kit";
3
+ import { Const, File, Type, jsxRenderer } from "kubb/jsx";
4
+ import { Fragment, jsx, jsxs } from "kubb/jsx/jsx-runtime";
5
+ //#region ../../internals/shared/src/params.ts
6
+ /**
7
+ * Drops parameters that share the same name, keeping the first.
8
+ *
9
+ * A malformed spec can declare the same parameter name twice within one `in` location. Both would
10
+ * resolve to the same output property, so emitting both would yield an object type with a duplicate
11
+ * member, which TypeScript rejects. This is a defensive guard against that case, not a casing guard:
12
+ * parameter names flow through unchanged, so no two distinct names ever collide here anymore.
13
+ */
14
+ function dedupeParams(params) {
15
+ const seen = /* @__PURE__ */ new Set();
16
+ return params.filter((param) => {
17
+ if (seen.has(param.name)) return false;
18
+ seen.add(param.name);
19
+ return true;
20
+ });
21
+ }
22
+ //#endregion
23
+ //#region ../../internals/shared/src/operation.ts
24
+ /**
25
+ * Maps a content type to the PascalCase suffix used to name per-content-type variants
26
+ * (e.g. `application/json` → `Json`, `application/xml` → `Xml`, `multipart/form-data` → `FormData`).
27
+ */
28
+ function getContentTypeSuffix(contentType) {
29
+ const baseType = contentType.split(";")[0].trim();
30
+ if (baseType === "application/json") return "Json";
31
+ if (baseType === "multipart/form-data") return "FormData";
32
+ if (baseType === "application/x-www-form-urlencoded") return "FormUrlEncoded";
33
+ const parts = (baseType.split("/").pop() ?? baseType).split(/[^a-zA-Z0-9]+/).filter(Boolean);
34
+ if (parts.length === 0) return "Unknown";
35
+ return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
36
+ }
37
+ /**
38
+ * Appends a content-type suffix to a base name, keeping a trailing `Data` segment last
39
+ * (e.g. `AddPetData` + `Json` → `AddPetJsonData`, `AddPetStatus200` + `Xml` → `AddPetStatus200Xml`).
40
+ */
41
+ function getPerContentTypeName(baseName, suffix) {
42
+ if (baseName.endsWith("Data")) return suffix.endsWith("Data") ? baseName.slice(0, -4) + suffix : `${baseName.slice(0, -4)}${suffix}Data`;
43
+ return baseName + suffix;
44
+ }
45
+ /**
46
+ * Resolves per-content-type variant names for a set of content entries, deduplicating suffix
47
+ * collisions with a numeric counter. Entries without a schema are skipped. The returned `suffix` is
48
+ * the final (possibly counter-augmented) value, so callers can derive parallel names in another
49
+ * namespace (e.g. plugin-faker deriving the matching plugin-ts type name).
50
+ */
51
+ function resolveContentTypeVariants(entries, baseName) {
52
+ const usedNames = /* @__PURE__ */ new Set();
53
+ return entries.filter((entry) => entry.schema).map((entry) => {
54
+ const baseSuffix = getContentTypeSuffix(entry.contentType);
55
+ let suffix = baseSuffix;
56
+ let name = getPerContentTypeName(baseName, suffix);
57
+ let counter = 2;
58
+ while (usedNames.has(name)) {
59
+ suffix = `${baseSuffix}${counter++}`;
60
+ name = getPerContentTypeName(baseName, suffix);
61
+ }
62
+ usedNames.add(name);
63
+ return {
64
+ name,
65
+ suffix,
66
+ schema: entry.schema,
67
+ keysToOmit: entry.keysToOmit,
68
+ contentType: entry.contentType
69
+ };
70
+ });
71
+ }
72
+ function getOperationParameters(node) {
73
+ return {
74
+ path: dedupeParams(node.parameters.filter((param) => param.in === "path")),
75
+ query: dedupeParams(node.parameters.filter((param) => param.in === "query")),
76
+ header: dedupeParams(node.parameters.filter((param) => param.in === "header")),
77
+ cookie: dedupeParams(node.parameters.filter((param) => param.in === "cookie"))
78
+ };
79
+ }
80
+ /**
81
+ * Builds the combined `{ body, path, query, headers }` options object schema for an operation,
82
+ * referencing the already-resolved body and grouped param names. Shared by `@kubb/plugin-ts`'s
83
+ * `Options` type and `@kubb/plugin-zod`'s inferred options schema, so both printers emit the same
84
+ * shape from the same inputs. `primitive: 'object'` is a no-op for the TS printer and tells the Zod
85
+ * printer to emit `z.object(…)` rather than a record.
86
+ */
87
+ function buildOptionsSchema(node, resolver) {
88
+ const { path, query, header } = getOperationParameters(node);
89
+ const hasBody = Boolean(node.requestBody?.content?.[0]?.schema);
90
+ const createNever = () => ast.factory.createSchema({
91
+ type: "never",
92
+ primitive: void 0,
93
+ optional: true
94
+ });
95
+ const groups = [
96
+ {
97
+ name: "path",
98
+ params: path,
99
+ resolve: resolver.param.path
100
+ },
101
+ {
102
+ name: "query",
103
+ params: query,
104
+ resolve: resolver.param.query
105
+ },
106
+ {
107
+ name: "headers",
108
+ params: header,
109
+ resolve: resolver.param.headers
110
+ }
111
+ ];
112
+ return ast.factory.createSchema({
113
+ type: "object",
114
+ primitive: "object",
115
+ deprecated: node.deprecated,
116
+ properties: [ast.factory.createProperty({
117
+ name: "body",
118
+ required: hasBody,
119
+ schema: hasBody ? ast.factory.createSchema({
120
+ type: "ref",
121
+ name: resolver.response.body(node)
122
+ }) : createNever()
123
+ }), ...groups.map(({ name, params, resolve }) => {
124
+ const required = params.some((param) => param.required);
125
+ return ast.factory.createProperty({
126
+ name,
127
+ required,
128
+ schema: params.length > 0 ? ast.factory.createSchema({
129
+ type: "ref",
130
+ name: resolve.call(resolver.param, node, params[0]),
131
+ optional: !required
132
+ }) : createNever()
133
+ });
134
+ })]
135
+ });
136
+ }
137
+ function getStatusCodeNumber(statusCode) {
138
+ const code = Number(statusCode);
139
+ return Number.isNaN(code) ? null : code;
140
+ }
141
+ function isSuccessStatusCode(statusCode) {
142
+ const code = getStatusCodeNumber(statusCode);
143
+ return code !== null && code >= 200 && code < 300;
144
+ }
145
+ function getSuccessResponses(responses) {
146
+ return responses.filter((response) => isSuccessStatusCode(response.statusCode));
147
+ }
148
+ //#endregion
149
+ //#region ../../internals/shared/src/adapter.ts
150
+ /**
151
+ * Narrows the generic `Adapter` from a generator context to the OpenAPI adapter,
152
+ * so OAS-only options (`dateType`, `enums`) and the parsed `document` are typed.
153
+ *
154
+ * Throws when a non-OAS adapter is configured, turning a silently wrong cast into a
155
+ * clear, actionable error at the point of use.
156
+ *
157
+ * @example
158
+ * ```ts
159
+ * const { dateType } = getOasAdapter(ctx.adapter).options
160
+ * ```
161
+ */
162
+ function getOasAdapter(adapter) {
163
+ if (adapter.name !== "oas") throw new Error(`Expected the OpenAPI adapter (adapterOas), but received "${adapter.name}". Configure \`adapter: adapterOas()\` in your Kubb config.`);
164
+ return adapter;
165
+ }
166
+ //#endregion
5
167
  //#region ../../internals/utils/src/casing.ts
6
168
  /**
7
169
  * Shared implementation for camelCase and PascalCase conversion.
@@ -13,61 +175,197 @@ import { Fragment, jsx, jsxs } from "@kubb/renderer-jsx/jsx-runtime";
13
175
  function toCamelOrPascal(text, pascal) {
14
176
  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
177
  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);
178
+ return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
18
179
  }).join("").replace(/[^a-zA-Z0-9]/g, "");
19
180
  }
20
181
  /**
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.
182
+ * Converts `text` to camelCase.
183
+ *
184
+ * @example Word boundaries
185
+ * `camelCase('hello-world') // 'helloWorld'`
24
186
  *
25
- * Only splits on dots followed by a letter so that version numbers
26
- * embedded in operationIds (e.g. `v2025.0`) are kept intact.
187
+ * @example With a prefix
188
+ * `camelCase('tag', { prefix: 'create' }) // 'createTag'`
27
189
  */
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("/");
190
+ function camelCase(text, { prefix = "", suffix = "" } = {}) {
191
+ return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
31
192
  }
32
193
  /**
33
- * Converts `text` to camelCase.
34
- * When `isFile` is `true`, dot-separated segments are each cased independently and joined with `/`.
194
+ * Converts `text` to PascalCase.
195
+ *
196
+ * @example Word boundaries
197
+ * `pascalCase('hello-world') // 'HelloWorld'`
198
+ *
199
+ * @example With a suffix
200
+ * `pascalCase('tag', { suffix: 'schema' }) // 'TagSchema'`
201
+ */
202
+ function pascalCase(text, { prefix = "", suffix = "" } = {}) {
203
+ return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
204
+ }
205
+ //#endregion
206
+ //#region ../../internals/utils/src/reserved.ts
207
+ /**
208
+ * JavaScript and Java reserved words.
209
+ * @link https://github.com/jonschlinkert/reserved/blob/master/index.js
210
+ */
211
+ const reservedWords = /* @__PURE__ */ new Set([
212
+ "abstract",
213
+ "arguments",
214
+ "boolean",
215
+ "break",
216
+ "byte",
217
+ "case",
218
+ "catch",
219
+ "char",
220
+ "class",
221
+ "const",
222
+ "continue",
223
+ "debugger",
224
+ "default",
225
+ "delete",
226
+ "do",
227
+ "double",
228
+ "else",
229
+ "enum",
230
+ "eval",
231
+ "export",
232
+ "extends",
233
+ "false",
234
+ "final",
235
+ "finally",
236
+ "float",
237
+ "for",
238
+ "function",
239
+ "goto",
240
+ "if",
241
+ "implements",
242
+ "import",
243
+ "in",
244
+ "instanceof",
245
+ "int",
246
+ "interface",
247
+ "let",
248
+ "long",
249
+ "native",
250
+ "new",
251
+ "null",
252
+ "package",
253
+ "private",
254
+ "protected",
255
+ "public",
256
+ "return",
257
+ "short",
258
+ "static",
259
+ "super",
260
+ "switch",
261
+ "synchronized",
262
+ "this",
263
+ "throw",
264
+ "throws",
265
+ "transient",
266
+ "true",
267
+ "try",
268
+ "typeof",
269
+ "var",
270
+ "void",
271
+ "volatile",
272
+ "while",
273
+ "with",
274
+ "yield",
275
+ "Array",
276
+ "Date",
277
+ "hasOwnProperty",
278
+ "Infinity",
279
+ "isFinite",
280
+ "isNaN",
281
+ "isPrototypeOf",
282
+ "length",
283
+ "Math",
284
+ "name",
285
+ "NaN",
286
+ "Number",
287
+ "Object",
288
+ "prototype",
289
+ "String",
290
+ "toString",
291
+ "undefined",
292
+ "valueOf"
293
+ ]);
294
+ /**
295
+ * Returns `true` when `name` is a syntactically valid JavaScript variable name.
35
296
  *
36
297
  * @example
37
- * camelCase('hello-world') // 'helloWorld'
38
- * camelCase('pet.petId', { isFile: true }) // 'pet/petId'
39
- */
40
- function camelCase(text, { isFile, prefix = "", suffix = "" } = {}) {
41
- if (isFile) return applyToFileParts(text, (part, isLast) => camelCase(part, isLast ? {
42
- prefix,
43
- suffix
44
- } : {}));
45
- return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
298
+ * ```ts
299
+ * isValidVarName('status') // true
300
+ * isValidVarName('class') // false (reserved word)
301
+ * isValidVarName('42foo') // false (starts with digit)
302
+ * ```
303
+ */
304
+ function isValidVarName(name) {
305
+ if (!name || reservedWords.has(name)) return false;
306
+ return isIdentifier(name);
46
307
  }
47
308
  /**
48
- * Converts `text` to PascalCase.
49
- * When `isFile` is `true`, the last dot-separated segment is PascalCased and earlier segments are camelCased.
309
+ * Returns `name` when it's a syntactically valid JavaScript variable name,
310
+ * otherwise prefixes it with `_` so the result is a valid identifier.
311
+ *
312
+ * Useful for sanitizing OpenAPI schema names or operation IDs that start with
313
+ * a digit (e.g. `409`, `504AccountCancel`) before using them as exported
314
+ * variable, type, or function names.
50
315
  *
51
316
  * @example
52
- * pascalCase('hello-world') // 'HelloWorld'
53
- * pascalCase('pet.petId', { isFile: true }) // 'pet/PetId'
54
- */
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));
60
- return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
317
+ * ```ts
318
+ * ensureValidVarName('409') // '_409'
319
+ * ensureValidVarName('504AccountCancel') // '_504AccountCancel'
320
+ * ensureValidVarName('Pet') // 'Pet'
321
+ * ensureValidVarName('class') // '_class'
322
+ * ```
323
+ */
324
+ function ensureValidVarName(name) {
325
+ if (!name || isValidVarName(name)) return name;
326
+ return `_${name}`;
327
+ }
328
+ /**
329
+ * Returns `true` when `name` is syntactically a valid identifier, ignoring reserved words.
330
+ *
331
+ * Reserved words and globals (`class`, `name`, `Date`, …) are valid as bare object-literal keys
332
+ * even though they are not valid variable names, so use this (not {@link isValidVarName}) when
333
+ * deciding whether an object key needs quoting.
334
+ *
335
+ * @example
336
+ * ```ts
337
+ * isIdentifier('name') // true
338
+ * isIdentifier('x-total')// false
339
+ * ```
340
+ */
341
+ function isIdentifier(name) {
342
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
61
343
  }
62
344
  //#endregion
63
- //#region ../../internals/utils/src/string.ts
345
+ //#region ../../internals/utils/src/strings.ts
346
+ /**
347
+ * Wraps a value in single quotes for emitting a single-quoted JavaScript string literal, escaping
348
+ * any backslash or single quote in the content.
349
+ *
350
+ * @example
351
+ * ```ts
352
+ * singleQuote('foo') // "'foo'"
353
+ * singleQuote("o'clock") // "'o\\'clock'"
354
+ * ```
355
+ */
356
+ function singleQuote(value) {
357
+ if (value === void 0 || value === null) return "''";
358
+ return `'${String(value).replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
359
+ }
64
360
  /**
65
361
  * Strips a single matching pair of `"..."`, `'...'`, or `` `...` `` from both ends of `text`.
66
362
  * Returns the string unchanged when no balanced quote pair is found.
67
363
  *
68
364
  * @example
365
+ * ```ts
69
366
  * trimQuotes('"hello"') // 'hello'
70
367
  * trimQuotes('hello') // 'hello'
368
+ * ```
71
369
  */
72
370
  function trimQuotes(text) {
73
371
  if (text.length >= 2) {
@@ -77,43 +375,32 @@ function trimQuotes(text) {
77
375
  }
78
376
  return text;
79
377
  }
80
- //#endregion
81
- //#region ../../internals/utils/src/object.ts
82
378
  /**
83
- * Serializes a primitive value to a JSON string literal, stripping any surrounding quote characters first.
379
+ * Serializes a primitive to a single-quoted string literal, stripping any surrounding quotes first.
84
380
  *
85
- * @example
86
- * stringify('hello') // '"hello"'
87
- * stringify('"hello"') // '"hello"'
88
- */
89
- function stringify(value) {
90
- if (value === void 0 || value === null) return "\"\"";
91
- return JSON.stringify(trimQuotes(value.toString()));
92
- }
93
- /**
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.
381
+ * Escaping runs through `JSON.stringify`, then the result switches to single quotes so the generated
382
+ * code matches the repo style without a formatter.
96
383
  *
97
384
  * @example
98
- * stringifyObject({ foo: 'bar', nested: { a: 1 } })
99
- * // 'foo: bar,\nnested: {\n a: 1\n }'
385
+ * ```ts
386
+ * stringify('hello') // "'hello'"
387
+ * stringify('"hello"') // "'hello'"
388
+ * ```
100
389
  */
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");
390
+ function stringify(value) {
391
+ if (value === void 0 || value === null) return "''";
392
+ return `'${JSON.stringify(trimQuotes(value.toString())).slice(1, -1).replace(/\\"/g, "\"").replace(/'/g, "\\'")}'`;
106
393
  }
107
- //#endregion
108
- //#region ../../internals/utils/src/regexp.ts
109
394
  /**
110
395
  * 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.
396
+ * Inline flags expressed as a `^(?im)` prefix are extracted and applied to the resulting expression.
112
397
  * Pass `null` as the second argument to emit a `/pattern/flags` literal instead.
113
398
  *
114
399
  * @example
115
- * toRegExpString('^(?im)foo') // → 'new RegExp("foo", "im")'
116
- * toRegExpString('^(?im)foo', null) // '/foo/im'
400
+ * ```ts
401
+ * toRegExpString('^(?im)foo') // 'new RegExp("^foo", "im")'
402
+ * toRegExpString('^(?im)foo', null) // '/^foo/im'
403
+ * ```
117
404
  */
118
405
  function toRegExpString(text, func = "RegExp") {
119
406
  const raw = trimQuotes(text);
@@ -126,83 +413,265 @@ function toRegExpString(text, func = "RegExp") {
126
413
  return `new ${func}(${JSON.stringify(source)}${flags ? `, ${JSON.stringify(flags)}` : ""})`;
127
414
  }
128
415
  //#endregion
129
- //#region src/components/Operations.tsx
130
- function Operations({ name, operations }) {
131
- const operationsJSON = operations.reduce((prev, acc) => {
132
- prev[`"${acc.node.operationId}"`] = acc.data;
133
- return prev;
134
- }, {});
135
- const pathsJSON = operations.reduce((prev, acc) => {
136
- prev[`"${acc.node.path}"`] = {
137
- ...prev[`"${acc.node.path}"`] ?? {},
138
- [acc.node.method]: `operations["${acc.node.operationId}"]`
139
- };
140
- return prev;
141
- }, {});
142
- return /* @__PURE__ */ jsxs(Fragment, { children: [
143
- /* @__PURE__ */ jsx(File.Source, {
144
- name: "OperationSchema",
145
- isExportable: true,
146
- isIndexable: true,
147
- children: /* @__PURE__ */ jsx(Type, {
148
- name: "OperationSchema",
149
- export: true,
150
- children: `{
151
- readonly request: z.ZodTypeAny | undefined;
152
- readonly parameters: {
153
- readonly path: z.ZodTypeAny | undefined;
154
- readonly query: z.ZodTypeAny | undefined;
155
- readonly header: z.ZodTypeAny | undefined;
156
- };
157
- readonly responses: {
158
- readonly [status: number]: z.ZodTypeAny;
159
- readonly default: z.ZodTypeAny;
160
- };
161
- readonly errors: {
162
- readonly [status: number]: z.ZodTypeAny;
163
- };
164
- }`
165
- })
166
- }),
167
- /* @__PURE__ */ jsx(File.Source, {
168
- name: "OperationsMap",
169
- isExportable: true,
170
- isIndexable: true,
171
- children: /* @__PURE__ */ jsx(Type, {
172
- name: "OperationsMap",
173
- export: true,
174
- children: "Record<string, OperationSchema>"
175
- })
176
- }),
177
- /* @__PURE__ */ jsx(File.Source, {
178
- name,
179
- isExportable: true,
180
- isIndexable: true,
181
- children: /* @__PURE__ */ jsx(Const, {
182
- export: true,
183
- name,
184
- asConst: true,
185
- children: `{${stringifyObject(operationsJSON)}}`
186
- })
187
- }),
188
- /* @__PURE__ */ jsx(File.Source, {
189
- name: "paths",
190
- isExportable: true,
191
- isIndexable: true,
192
- children: /* @__PURE__ */ jsx(Const, {
193
- export: true,
194
- name: "paths",
195
- asConst: true,
196
- children: `{${stringifyObject(pathsJSON)}}`
197
- })
198
- })
199
- ] });
416
+ //#region ../../internals/utils/src/codegen.ts
417
+ const INDENT = " ";
418
+ /**
419
+ * Indents every non-empty line of `text` by one indent level, leaving blank lines empty.
420
+ */
421
+ function indentLines(text) {
422
+ if (!text) return "";
423
+ return text.split("\n").map((line) => line.trim() ? `${INDENT}${line}` : "").join("\n");
424
+ }
425
+ /**
426
+ * Renders an object key, quoting it with single quotes only when it is not a valid identifier.
427
+ * Reserved words and globals (`name`, `class`, …) are valid bare keys and stay unquoted.
428
+ *
429
+ * @example
430
+ * ```ts
431
+ * objectKey('name') // 'name'
432
+ * objectKey('x-total') // "'x-total'"
433
+ * ```
434
+ */
435
+ function objectKey(name) {
436
+ return isIdentifier(name) ? name : singleQuote(name);
437
+ }
438
+ /**
439
+ * Assembles a multi-line object literal from already-rendered `entries`, indenting each entry one
440
+ * level and closing the brace at column zero. Entries that are themselves multi-line objects indent
441
+ * cumulatively. Each entry ends with a trailing comma to match the formatter's multi-line style.
442
+ *
443
+ * @example
444
+ * ```ts
445
+ * buildObject(['id: z.number()', 'name: z.string()'])
446
+ * // '{\n id: z.number(),\n name: z.string(),\n}'
447
+ * ```
448
+ */
449
+ function buildObject(entries) {
450
+ if (entries.length === 0) return "{}";
451
+ return `{\n${entries.map((entry) => `${indentLines(entry)},`).join("\n")}\n}`;
452
+ }
453
+ /**
454
+ * Assembles a bracketed list (array by default) from already-rendered `items`. Keeps everything on
455
+ * one line when no item spans multiple lines, and otherwise puts each item on its own line, indented
456
+ * one level with a trailing comma and the closing bracket at column zero. Used for member lists such
457
+ * as `z.union([…])` and `z.array([…])`.
458
+ *
459
+ * @example
460
+ * ```ts
461
+ * buildList(['z.string()', 'z.number()'])
462
+ * // '[z.string(), z.number()]'
463
+ * ```
464
+ */
465
+ function buildList(items, brackets = ["[", "]"]) {
466
+ const [open, close] = brackets;
467
+ if (items.length === 0) return `${open}${close}`;
468
+ if (!items.some((item) => item.includes("\n"))) return `${open}${items.join(", ")}${close}`;
469
+ return `${open}\n${items.map((item) => `${indentLines(item)},`).join("\n")}\n${close}`;
470
+ }
471
+ /**
472
+ * Emits a lazy getter for a circular-ref property position, `get name() { return body }`. The key
473
+ * is quoted only when it is not a valid identifier. Used by the string printers to defer evaluation
474
+ * of a recursive schema until first access.
475
+ *
476
+ * @example
477
+ * ```ts
478
+ * lazyGetter({ name: 'parent', body: 'z.lazy(() => Pet)' })
479
+ * // "get parent() { return z.lazy(() => Pet) }"
480
+ * ```
481
+ */
482
+ function lazyGetter({ name, body }) {
483
+ return `get ${objectKey(name)}() { return ${body} }`;
484
+ }
485
+ //#endregion
486
+ //#region ../../internals/utils/src/fs.ts
487
+ /**
488
+ * Builds a nested file path from a dotted name. Splits on dots that precede a letter
489
+ * (so version numbers embedded in operationIds like `v2025.0` stay intact), camelCases
490
+ * every earlier segment, applies `caseLast` to the final segment, and joins with `/`.
491
+ *
492
+ * Empty segments are dropped before joining. They arise when the name starts with a dot
493
+ * followed by a letter (e.g. `..Schema` splits into `['..', 'Schema']` and `'..'` cases to
494
+ * an empty string). Without this a leading `/` would form, which `path.resolve` reads as an
495
+ * absolute path, letting generated files escape the configured output directory.
496
+ *
497
+ * @example Nested path from a dotted name
498
+ * `toFilePath('pet.petId') // 'pet/petId'`
499
+ *
500
+ * @example PascalCase the final segment
501
+ * `toFilePath('pet.Pet', pascalCase) // 'pet/Pet'`
502
+ *
503
+ * @example Suffix applied to the final segment only
504
+ * `toFilePath('tag.tag', (part) => camelCase(part, { suffix: 'schema' })) // 'tag/tagSchema'`
505
+ */
506
+ function toFilePath(name, caseLast = camelCase) {
507
+ const parts = name.split(/\.(?=[a-zA-Z])/);
508
+ return parts.map((part, i) => i === parts.length - 1 ? caseLast(part) : camelCase(part)).filter(Boolean).join("/");
509
+ }
510
+ //#endregion
511
+ //#region ../../internals/shared/src/resolver.ts
512
+ /**
513
+ * Resolves a single operation parameter name with the
514
+ * `<operationId> <in> <name>` template.
515
+ *
516
+ * @example
517
+ * `operationParamName.call(resolver, node, param) // → 'DeletePetPathPetId'`
518
+ */
519
+ function operationParamName(node, param) {
520
+ return this.name(`${node.operationId} ${param.in} ${param.name}`);
521
+ }
522
+ /**
523
+ * Builds the shared `param` namespace. Spread the result into `createResolver`
524
+ * and override individual methods next to it when a plugin deviates.
525
+ *
526
+ * @example
527
+ * ```ts
528
+ * createResolver<PluginTs>({ param: createOperationParamResolver(), ... })
529
+ * ```
530
+ */
531
+ function createOperationParamResolver() {
532
+ return {
533
+ name: operationParamName,
534
+ path(node) {
535
+ return this.name(`${node.operationId} Path`);
536
+ },
537
+ query(node) {
538
+ return this.name(`${node.operationId} Query`);
539
+ },
540
+ headers(node) {
541
+ return this.name(`${node.operationId} Headers`);
542
+ }
543
+ };
544
+ }
545
+ /**
546
+ * Builds the shared `response` namespace. Spread the result into
547
+ * `createResolver` and add plugin-specific methods (`options`, `error`) next
548
+ * to it.
549
+ *
550
+ * @example
551
+ * ```ts
552
+ * createResolver<PluginTs>({ response: { ...createOperationResponseResolver(), options(node) {...} }, ... })
553
+ * ```
554
+ */
555
+ function createOperationResponseResolver() {
556
+ return {
557
+ status(node, statusCode) {
558
+ return this.name(`${node.operationId} Status ${statusCode}`);
559
+ },
560
+ body(node) {
561
+ return this.name(`${node.operationId} Body`);
562
+ },
563
+ responses(node) {
564
+ return this.name(`${node.operationId} Responses`);
565
+ },
566
+ response(node) {
567
+ return this.name(`${node.operationId} Response`);
568
+ }
569
+ };
570
+ }
571
+ /**
572
+ * Builds a resolver `file` override whose base name runs every path segment
573
+ * through `toFilePath`, casing the final segment with `caseLast`.
574
+ *
575
+ * @example
576
+ * ```ts
577
+ * createResolver<PluginTs>({ file: createCasedFile(pascalCase), ... })
578
+ * ```
579
+ */
580
+ function createCasedFile(caseLast) {
581
+ return { baseName({ name, extname }) {
582
+ return `${toFilePath(name, caseLast)}${extname}`;
583
+ } };
584
+ }
585
+ //#endregion
586
+ //#region ../../internals/shared/src/refs.ts
587
+ /**
588
+ * Collects the resolved target name of every pointer-carrying ref in a schema tree, in
589
+ * first-occurrence order. Use this for name-only checks (e.g. redeclaration detection) where
590
+ * `resolver.imports` would resolve file paths that are then discarded.
591
+ */
592
+ function collectRefNames(schema) {
593
+ return ast.collectSync(schema, { schema: (node) => {
594
+ const refNode = ast.narrowSchema(node, "ref");
595
+ if (!refNode?.ref) return null;
596
+ return ast.resolveRefName(refNode);
597
+ } });
598
+ }
599
+ //#endregion
600
+ //#region ../../internals/shared/src/group.ts
601
+ /**
602
+ * Builds the `group` config a Kubb plugin passes to `ctx.setOptions`, applying the
603
+ * shared default naming so every plugin groups output consistently:
604
+ *
605
+ * - `path` groups use the second path segment (`/pet/findByStatus` → `pet`).
606
+ * - other groups use the camelCased group (`pet store` → `petStore`).
607
+ *
608
+ * A user-provided `group.name` always wins over the default namer, so callers stay in
609
+ * control of their output folders. Returns `null` when grouping is disabled, matching the
610
+ * per-plugin convention.
611
+ *
612
+ * @param group - The user-supplied group option, or `undefined` to disable grouping.
613
+ *
614
+ * @example
615
+ * ```ts
616
+ * createGroupConfig(group) // shared across every plugin
617
+ * ```
618
+ */
619
+ function createGroupConfig(group) {
620
+ if (!group) return null;
621
+ const defaultName = (ctx) => {
622
+ if (group.type === "path") return `${ctx.group.split("/")[1]}`;
623
+ return camelCase(ctx.group);
624
+ };
625
+ return {
626
+ ...group,
627
+ name: group.name ? group.name : defaultName
628
+ };
629
+ }
630
+ //#endregion
631
+ //#region ../../internals/shared/src/schemaTraversal.ts
632
+ /**
633
+ * Maps each property of an object schema to its transformed output. Pairs every result with the
634
+ * original property so the printer keeps full control over modifiers, getters, and key syntax.
635
+ *
636
+ * @example
637
+ * ```ts
638
+ * const entries = mapSchemaProperties(node, (schema) => this.transform(schema))
639
+ * // entries: [{ name: 'id', property, output: 'z.number()' }, ...]
640
+ * ```
641
+ */
642
+ function mapSchemaProperties(node, transform) {
643
+ return node.properties.map((property) => ({
644
+ name: property.name,
645
+ property,
646
+ output: transform(property.schema)
647
+ }));
648
+ }
649
+ /**
650
+ * Maps each member of a union or intersection schema to its transformed output, pairing every
651
+ * result with the original member.
652
+ */
653
+ function mapSchemaMembers(node, transform) {
654
+ return (node.members ?? []).map((schema) => ({
655
+ schema,
656
+ output: transform(schema)
657
+ }));
658
+ }
659
+ /**
660
+ * Maps each item of an array or tuple schema to its transformed output, pairing every result with
661
+ * the original item.
662
+ */
663
+ function mapSchemaItems(node, transform) {
664
+ return (node.items ?? []).map((schema) => ({
665
+ schema,
666
+ output: transform(schema)
667
+ }));
200
668
  }
201
669
  //#endregion
202
670
  //#region src/components/Zod.tsx
203
- function Zod({ name, node, printer, inferTypeName }) {
671
+ function Zod({ name, node, printer, inferTypeName, cyclic }) {
204
672
  const output = printer.print(node);
205
673
  if (!output) return;
674
+ const needsAnnotation = cyclic && node.type !== "object";
206
675
  return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(File.Source, {
207
676
  name,
208
677
  isExportable: true,
@@ -210,6 +679,7 @@ function Zod({ name, node, printer, inferTypeName }) {
210
679
  children: /* @__PURE__ */ jsx(Const, {
211
680
  export: true,
212
681
  name,
682
+ type: needsAnnotation ? "z.ZodType" : void 0,
213
683
  children: output
214
684
  })
215
685
  }), inferTypeName && /* @__PURE__ */ jsx(File.Source, {
@@ -230,7 +700,7 @@ function Zod({ name, node, printer, inferTypeName }) {
230
700
  * Import paths that use a namespace import (`import * as z from '...'`).
231
701
  * All other import paths use a named import (`import { z } from '...'`).
232
702
  */
233
- const ZOD_NAMESPACE_IMPORTS = new Set(["zod", "zod/mini"]);
703
+ const ZOD_NAMESPACE_IMPORTS = /* @__PURE__ */ new Set(["zod", "zod/mini"]);
234
704
  //#endregion
235
705
  //#region src/utils.ts
236
706
  /**
@@ -242,34 +712,106 @@ function shouldCoerce(coercion, type) {
242
712
  return !!coercion[type];
243
713
  }
244
714
  /**
245
- * Collects all resolved schema names for an operation's parameters and responses
246
- * into a single lookup object, useful for building imports and type references.
247
- */
248
- function buildSchemaNames(node, { params, resolver }) {
249
- const pathParam = params.find((p) => p.in === "path");
250
- const queryParam = params.find((p) => p.in === "query");
251
- const headerParam = params.find((p) => p.in === "header");
252
- const responses = {};
253
- const errors = {};
254
- for (const res of node.responses) {
255
- const name = resolver.resolveResponseStatusName(node, res.statusCode);
256
- const statusNum = Number(res.statusCode);
257
- if (!Number.isNaN(statusNum)) {
258
- responses[statusNum] = name;
259
- if (statusNum >= 400) errors[statusNum] = name;
715
+ * Registered codecs, checked in order.
716
+ */
717
+ const codecs = [{
718
+ matches(node) {
719
+ return node.type === "date" && node.representation === "date";
720
+ },
721
+ decode(node) {
722
+ return node.format === "date" ? "z.iso.date().transform((value) => new Date(value))" : "z.iso.datetime().transform((value) => new Date(value))";
723
+ },
724
+ encode(node) {
725
+ return node.format === "date" ? "z.date().transform((value) => value.toISOString().slice(0, 10))" : "z.date().transform((value) => value.toISOString())";
726
+ }
727
+ }];
728
+ /**
729
+ * Returns the codec for this node, or `undefined` when the node needs no
730
+ * encode/decode (its wire and runtime types match).
731
+ */
732
+ function getCodec(node) {
733
+ if (!node) return void 0;
734
+ return codecs.find((codec) => codec.matches(node));
735
+ }
736
+ /**
737
+ * Returns `true` when the node itself is encoded/decoded by a codec.
738
+ */
739
+ function hasCodec(node) {
740
+ return getCodec(node) !== void 0;
741
+ }
742
+ /**
743
+ * Returns `true` when the schema transitively contains a codec node —
744
+ * a value whose runtime type differs from its wire type (see {@link hasCodec}),
745
+ * so it must be decoded (response) or encoded (request) at the validation boundary.
746
+ * `$ref`s are followed via their resolved schema; a `seen` set guards cycles.
747
+ */
748
+ function containsCodec(node, seen = /* @__PURE__ */ new Set()) {
749
+ if (!node) return false;
750
+ if (hasCodec(node)) return true;
751
+ if (node.type === "ref") {
752
+ if (!node.ref) return false;
753
+ const refName = extractRefName(node.ref);
754
+ if (refName) {
755
+ if (seen.has(refName)) return false;
756
+ seen.add(refName);
260
757
  }
758
+ const resolved = syncSchemaRef(node);
759
+ if (resolved.type === "ref") return false;
760
+ return containsCodec(resolved, seen);
261
761
  }
262
- responses["default"] = resolver.resolveResponseName(node);
263
- return {
264
- request: node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : void 0,
265
- 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
269
- },
270
- responses,
271
- errors
272
- };
762
+ const children = [];
763
+ if ("properties" in node && node.properties) children.push(...node.properties.map((prop) => prop.schema));
764
+ if ("items" in node && node.items) children.push(...node.items);
765
+ if ("members" in node && node.members) children.push(...node.members);
766
+ if ("additionalProperties" in node && node.additionalProperties && node.additionalProperties !== true) children.push(node.additionalProperties);
767
+ return children.some((child) => containsCodec(child, seen));
768
+ }
769
+ /**
770
+ * Collects the names of `$ref` schemas that transitively contain a codec, so the generator can route
771
+ * them to their input (encode) variant.
772
+ */
773
+ function collectCodecRefNames(node) {
774
+ return ast.collectSync(node, { schema: (n) => n.type === "ref" && n.ref && containsCodec(n) ? ast.resolveRefName(n) ?? void 0 : void 0 });
775
+ }
776
+ /**
777
+ * Whether the node is a plain inline object whose shape can be lifted into an `.extend({ … })`
778
+ * argument. A catchall, `patternProperties`, or a nullable/optional wrapper cannot, so those stay
779
+ * on `.and(…)`.
780
+ */
781
+ function isPlainInlineObject(node) {
782
+ return node.type === "object" && !node.nullable && !node.optional && !node.nullish && node.additionalProperties === void 0 && !node.patternProperties;
783
+ }
784
+ /**
785
+ * Whether a node renders as a bare Zod object — one that accepts `.extend(…)` and can be a
786
+ * `z.discriminatedUnion` option. Covers object nodes, `$ref`s that resolve to (or are still
787
+ * unresolved) objects, single-member unions of an object, and object-composable `allOf`.
788
+ *
789
+ * `cyclicSchemas` bounds the recursion: a ref into a cycle renders as `z.lazy(…)`, not an object.
790
+ */
791
+ function isObjectSchemaNode(node, cyclicSchemas) {
792
+ if (node.nullable || node.optional || node.nullish) return false;
793
+ if (node.type === "object") return true;
794
+ if (node.type === "ref") {
795
+ const refName = ast.resolveRefName(node);
796
+ if (refName && cyclicSchemas?.has(refName)) return false;
797
+ const resolved = syncSchemaRef(node);
798
+ return resolved.type === "ref" || isObjectSchemaNode(resolved, cyclicSchemas);
799
+ }
800
+ if (node.type === "union") {
801
+ const members = node.members ?? [];
802
+ return members.length === 1 && isObjectSchemaNode(members[0], cyclicSchemas);
803
+ }
804
+ return isObjectComposableIntersection(node, cyclicSchemas);
805
+ }
806
+ /**
807
+ * Whether an `allOf` is a pure object composition — an object base plus inline object members whose
808
+ * shapes merge with `.extend(…)`. These stay a Zod object instead of a `ZodIntersection`, which
809
+ * `z.discriminatedUnion` rejects.
810
+ */
811
+ function isObjectComposableIntersection(node, cyclicSchemas) {
812
+ if (node.type !== "intersection") return false;
813
+ const [first, ...rest] = node.members ?? [];
814
+ return !!first && isObjectSchemaNode(first, cyclicSchemas) && rest.every(isPlainInlineObject);
273
815
  }
274
816
  /**
275
817
  * Format a default value as a code-level literal.
@@ -281,6 +823,33 @@ function formatDefault(value) {
281
823
  return String(value ?? "");
282
824
  }
283
825
  /**
826
+ * Format a default for `.default(...)` so the literal matches the generated schema's type.
827
+ *
828
+ * An OpenAPI `default` does not always agree with the schema it sits on: a `bigint` field
829
+ * (`format: int64`) carries a `number` default, and a spec may put `default: {}` on an `array`.
830
+ * Emitting those verbatim produces a Zod schema that does not typecheck, so coerce the bigint case
831
+ * to a `BigInt(...)` literal and emit an array literal for arrays (dropping a non-array default).
832
+ * Returns `null` when no `.default(...)` should be emitted. Keys off the schema node's type.
833
+ */
834
+ function defaultLiteral(node, value) {
835
+ if (value === null) return null;
836
+ if (node && ast.narrowSchema(node, "bigint")) {
837
+ if (typeof value === "bigint") return `BigInt(${value})`;
838
+ if (typeof value === "number" && Number.isInteger(value)) return `BigInt(${value})`;
839
+ return null;
840
+ }
841
+ if (node && ast.narrowSchema(node, "array")) return Array.isArray(value) ? JSON.stringify(value) : null;
842
+ const enumNode = node ? ast.narrowSchema(node, "enum") : void 0;
843
+ if (enumNode) {
844
+ const values = enumNode.namedEnumValues?.map((member) => member.value) ?? enumNode.enumValues ?? [];
845
+ if (values.length) {
846
+ const match = values.find((member) => member === value || String(member) === String(value));
847
+ return match != null ? formatLiteral(match) : null;
848
+ }
849
+ }
850
+ return formatDefault(value);
851
+ }
852
+ /**
284
853
  * Format a primitive enum/literal value.
285
854
  * Strings are quoted; numbers and booleans are emitted raw.
286
855
  */
@@ -289,6 +858,47 @@ function formatLiteral(v) {
289
858
  return String(v);
290
859
  }
291
860
  /**
861
+ * Build the Zod schema for a set of literal enum values.
862
+ * `z.enum()` only accepts string members in Zod v4, so a numeric, boolean, or
863
+ * mixed set is emitted as a single `z.literal(…)` or a `z.union([z.literal(…), …])`.
864
+ * An all-string set keeps the more compact `z.enum([…])`.
865
+ */
866
+ function buildEnum(values) {
867
+ if (values.every((v) => typeof v === "string")) return `z.enum([${values.map(formatLiteral).join(", ")}])`;
868
+ const literals = values.map((v) => `z.literal(${formatLiteral(v)})`);
869
+ if (literals.length === 1) return literals[0];
870
+ return `z.union([${literals.join(", ")}])`;
871
+ }
872
+ /**
873
+ * Map a `regexType` to the `func` argument of `toRegExpString`: `'constructor'` emits
874
+ * `new RegExp(...)`, while `'literal'` (the default) emits a regex literal.
875
+ */
876
+ function regexFunc(regexType) {
877
+ return regexType === "constructor" ? "RegExp" : null;
878
+ }
879
+ /**
880
+ * Builds a Zod record key schema that enforces the `patternProperties` regexes a plain
881
+ * `.catchall()` would drop. Several patterns combine into one alternation (`(^a)|(^b)`).
882
+ */
883
+ function patternKeySchema({ patterns, regexType }) {
884
+ return `z.string().regex(${patternKeySource({
885
+ patterns,
886
+ regexType
887
+ })})`;
888
+ }
889
+ /**
890
+ * `zod/mini` variant of {@link patternKeySchema}, wrapping the regex in `.check(z.regex(...))`.
891
+ */
892
+ function patternKeySchemaMini({ patterns, regexType }) {
893
+ return `z.string().check(z.regex(${patternKeySource({
894
+ patterns,
895
+ regexType
896
+ })}))`;
897
+ }
898
+ function patternKeySource({ patterns, regexType }) {
899
+ return toRegExpString(patterns.length === 1 ? patterns[0] : patterns.map((pattern) => `(${pattern})`).join("|"), regexFunc(regexType));
900
+ }
901
+ /**
292
902
  * Build `.min()` / `.max()` / `.gt()` / `.lt()` constraint chains for numbers
293
903
  * using the standard chainable Zod v4 API.
294
904
  */
@@ -305,11 +915,11 @@ function numberConstraints({ min, max, exclusiveMinimum, exclusiveMaximum, multi
305
915
  * Build `.min()` / `.max()` / `.regex()` chains for strings/arrays
306
916
  * using the standard chainable Zod v4 API.
307
917
  */
308
- function lengthConstraints({ min, max, pattern }) {
918
+ function lengthConstraints({ min, max, pattern, regexType }) {
309
919
  return [
310
920
  min !== void 0 ? `.min(${min})` : "",
311
921
  max !== void 0 ? `.max(${max})` : "",
312
- pattern !== void 0 ? `.regex(${toRegExpString(pattern, null)})` : ""
922
+ pattern !== void 0 ? `.regex(${toRegExpString(pattern, regexFunc(regexType))})` : ""
313
923
  ].join("");
314
924
  }
315
925
  /**
@@ -327,52 +937,140 @@ function numberChecksMini({ min, max, exclusiveMinimum, exclusiveMaximum, multip
327
937
  /**
328
938
  * Build `.check(z.minLength(), z.maxLength(), z.regex())` for `zod/mini` length constraints.
329
939
  */
330
- function lengthChecksMini({ min, max, pattern }) {
940
+ function lengthChecksMini({ min, max, pattern, regexType }) {
331
941
  const checks = [];
332
942
  if (min !== void 0) checks.push(`z.minLength(${min})`);
333
943
  if (max !== void 0) checks.push(`z.maxLength(${max})`);
334
- if (pattern !== void 0) checks.push(`z.regex(${toRegExpString(pattern, null)})`);
944
+ if (pattern !== void 0) checks.push(`z.regex(${toRegExpString(pattern, regexFunc(regexType))})`);
335
945
  return checks.length ? `.check(${checks.join(", ")})` : "";
336
946
  }
337
947
  /**
338
- * Apply nullable / optional / nullish modifiers and an optional `.describe()` call
339
- * to a schema value string using the chainable Zod v4 API.
948
+ * Apply nullable / optional / nullish modifiers, an optional `.describe()` call, and an
949
+ * optional `.meta({ examples })` call to a schema value string using the chainable Zod v4 API.
340
950
  */
341
- 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;
951
+ function applyModifiers({ value, schema, nullable, optional, nullish, defaultValue, description, examples }) {
952
+ const withModifier = (() => {
953
+ if (nullish || nullable && optional) return `${value}.nullish()`;
954
+ if (optional) return `${value}.optional()`;
955
+ if (nullable) return `${value}.nullable()`;
956
+ return value;
957
+ })();
958
+ const literal = defaultValue !== void 0 ? defaultLiteral(schema, defaultValue) : null;
959
+ const withDefault = literal !== null ? `${withModifier}.default(${literal})` : withModifier;
960
+ const withDescription = description ? `${withDefault}.describe(${stringify(description)})` : withDefault;
961
+ return examples?.length ? `${withDescription}.meta({ examples: [${examples.map(formatDefault).join(", ")}] })` : withDescription;
962
+ }
963
+ function modifierDepth(schema) {
964
+ if (schema.nullish || schema.nullable && schema.optional) return 2;
965
+ if (schema.nullable || schema.optional) return 1;
966
+ return 0;
967
+ }
968
+ /**
969
+ * Build the `.unwrap()` chain to insert before `.omit()` when the schema is a `$ref`.
970
+ *
971
+ * A `$ref` resolves to a named schema variable that already carries its own `.nullable()` /
972
+ * `.optional()` / `.nullish()` / `.default()` wrappers. `.omit()` lives on the inner `ZodObject`,
973
+ * not on those `ZodNullable` / `ZodOptional` / `ZodDefault` wrappers, so the omit has to unwrap
974
+ * down to the object first. This mirrors plugin-ts emitting `Omit<NonNullable<T>, …>`. Inline
975
+ * objects need no unwrap because the printer adds their modifiers after `.omit()`.
976
+ */
977
+ function omitUnwrapChain(node) {
978
+ const ref = ast.narrowSchema(node, "ref");
979
+ if (!ref) return "";
980
+ const target = ref.schema ?? ref;
981
+ const depth = modifierDepth(target) + (target.default !== void 0 ? 1 : 0);
982
+ return ".unwrap()".repeat(depth);
349
983
  }
350
984
  /**
351
985
  * Apply nullable / optional / nullish modifiers using the functional `zod/mini` API
352
986
  * (`z.nullable()`, `z.optional()`, `z.nullish()`).
353
987
  */
354
- 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;
988
+ function applyMiniModifiers({ value, schema, nullable, optional, nullish, defaultValue }) {
989
+ const withModifier = (() => {
990
+ if (nullish) return `z.nullish(${value})`;
991
+ const withNullable = nullable ? `z.nullable(${value})` : value;
992
+ return optional ? `z.optional(${withNullable})` : withNullable;
993
+ })();
994
+ const literal = defaultValue !== void 0 ? defaultLiteral(schema, defaultValue) : null;
995
+ return literal !== null ? `z._default(${withModifier}, ${literal})` : withModifier;
996
+ }
997
+ /**
998
+ * Builds an `object` schema node grouping the given parameter nodes.
999
+ * The `primitive: 'object'` marker ensures the Zod printer emits `z.object(…)` rather than a record.
1000
+ */
1001
+ function buildGroupedParamsSchema({ params, optional }) {
1002
+ return ast.factory.createSchema({
1003
+ type: "object",
1004
+ optional,
1005
+ primitive: "object",
1006
+ properties: params.map((param) => ast.factory.createProperty({
1007
+ name: param.name,
1008
+ required: param.required,
1009
+ schema: param.schema
1010
+ }))
1011
+ });
363
1012
  }
364
1013
  //#endregion
365
1014
  //#region src/printers/printerZod.ts
366
- function strictOneOfMember$1(member, node) {
1015
+ function strictOneOfMember$1(member, node, cyclicSchemas) {
367
1016
  if (node.type === "object" && node.additionalProperties === void 0) return `${member}.strict()`;
368
1017
  if (node.type === "ref") {
369
1018
  if (member.startsWith("z.lazy(")) return member;
370
- const schema = ast.syncSchemaRef(node);
1019
+ const refName = ast.resolveRefName(node);
1020
+ if (refName && cyclicSchemas?.has(refName)) return member;
1021
+ const schema = syncSchemaRef(node);
1022
+ if (schema.nullable || schema.optional || node.nullable || node.optional) return member;
371
1023
  if (schema.type === "object" && (schema.additionalProperties === void 0 || schema.additionalProperties === false)) return `${member}.strict()`;
372
1024
  }
373
1025
  return member;
374
1026
  }
375
1027
  __name(strictOneOfMember$1, "strictOneOfMember");
1028
+ function getMemberConstraint({ member, regexType }) {
1029
+ if (member.primitive === "string") return lengthConstraints({
1030
+ ...ast.narrowSchema(member, "string") ?? {},
1031
+ regexType
1032
+ }) || void 0;
1033
+ if (member.primitive === "number" || member.primitive === "integer") return numberConstraints(ast.narrowSchema(member, "number") ?? ast.narrowSchema(member, "integer") ?? {}) || void 0;
1034
+ if (member.primitive === "array") return lengthConstraints({
1035
+ ...ast.narrowSchema(member, "array") ?? {},
1036
+ regexType
1037
+ }) || void 0;
1038
+ }
1039
+ /**
1040
+ * Builds the `{ key: value, … }` shape for an object node, shared by the `z.object(...)` and
1041
+ * `.extend(...)` renderings so they stay in lockstep.
1042
+ */
1043
+ function buildZodObjectShape(ctx, node) {
1044
+ const objectNode = ast.narrowSchema(node, "object");
1045
+ if (!objectNode) return "{}";
1046
+ const isCyclic = (schema) => ctx.options.cyclicSchemas != null && containsCircularRef(schema, { circularSchemas: ctx.options.cyclicSchemas });
1047
+ return buildObject(mapSchemaProperties(objectNode, (schema) => {
1048
+ const hasSelfRef = isCyclic(schema);
1049
+ const savedCyclicSchemas = ctx.options.cyclicSchemas;
1050
+ if (hasSelfRef) ctx.options.cyclicSchemas = void 0;
1051
+ const baseOutput = ctx.transform(schema) ?? ctx.transform(ast.factory.createSchema({ type: "unknown" }));
1052
+ if (hasSelfRef) ctx.options.cyclicSchemas = savedCyclicSchemas;
1053
+ return baseOutput;
1054
+ }).map(({ name: propName, property, output: baseOutput }) => {
1055
+ const { schema } = property;
1056
+ const meta = syncSchemaRef(schema);
1057
+ const descriptionToApply = schema.type !== "ref" && meta.type === "ref" ? void 0 : meta.description;
1058
+ const value = applyModifiers({
1059
+ value: baseOutput,
1060
+ schema,
1061
+ nullable: meta.nullable,
1062
+ optional: schema.optional || property.required === false,
1063
+ nullish: schema.nullish,
1064
+ defaultValue: meta.default,
1065
+ description: descriptionToApply,
1066
+ examples: meta.examples
1067
+ });
1068
+ return isCyclic(schema) ? lazyGetter({
1069
+ name: propName,
1070
+ body: value
1071
+ }) : `${objectKey(propName)}: ${value}`;
1072
+ }));
1073
+ }
376
1074
  /**
377
1075
  * Zod v4 printer built with `definePrinter`.
378
1076
  *
@@ -385,7 +1083,8 @@ __name(strictOneOfMember$1, "strictOneOfMember");
385
1083
  * const code = printer.print(stringNode) // "z.string()"
386
1084
  * ```
387
1085
  */
388
- const printerZod = ast.definePrinter((options) => {
1086
+ const printerZod = ast.createPrinter((options) => {
1087
+ const cyclicSchemaNames = options.cyclicSchemas;
389
1088
  return {
390
1089
  name: "zod",
391
1090
  options,
@@ -397,7 +1096,10 @@ const printerZod = ast.definePrinter((options) => {
397
1096
  boolean: () => "z.boolean()",
398
1097
  null: () => "z.null()",
399
1098
  string(node) {
400
- return `${shouldCoerce(this.options.coercion, "strings") ? "z.coerce.string()" : "z.string()"}${lengthConstraints(node)}`;
1099
+ return `${shouldCoerce(this.options.coercion, "strings") ? "z.coerce.string()" : "z.string()"}${lengthConstraints({
1100
+ ...node,
1101
+ regexType: this.options.regexType
1102
+ })}`;
401
1103
  },
402
1104
  number(node) {
403
1105
  return `${shouldCoerce(this.options.coercion, "numbers") ? "z.coerce.number()" : "z.number()"}${numberConstraints(node)}`;
@@ -409,8 +1111,12 @@ const printerZod = ast.definePrinter((options) => {
409
1111
  return shouldCoerce(this.options.coercion, "numbers") ? "z.coerce.bigint()" : "z.bigint()";
410
1112
  },
411
1113
  date(node) {
412
- if (node.representation === "string") return "z.iso.date()";
413
- return shouldCoerce(this.options.coercion, "dates") ? "z.coerce.date()" : "z.date()";
1114
+ const codec = getCodec(node);
1115
+ if (codec) {
1116
+ if (this.options.direction === "input") return codec.encode(node);
1117
+ return shouldCoerce(this.options.coercion, "dates") ? "z.coerce.date()" : codec.decode(node);
1118
+ }
1119
+ return "z.iso.date()";
414
1120
  },
415
1121
  datetime(node) {
416
1122
  const offset = node.offset || this.options.dateType === "stringOffset";
@@ -424,13 +1130,22 @@ const printerZod = ast.definePrinter((options) => {
424
1130
  return shouldCoerce(this.options.coercion, "dates") ? "z.coerce.date()" : "z.date()";
425
1131
  },
426
1132
  uuid(node) {
427
- return `${this.options.guidType === "guid" ? "z.guid()" : "z.uuid()"}${lengthConstraints(node)}`;
1133
+ return `${this.options.guidType === "guid" ? "z.guid()" : "z.uuid()"}${lengthConstraints({
1134
+ ...node,
1135
+ regexType: this.options.regexType
1136
+ })}`;
428
1137
  },
429
1138
  email(node) {
430
- return `z.email()${lengthConstraints(node)}`;
1139
+ return `z.email()${lengthConstraints({
1140
+ ...node,
1141
+ regexType: this.options.regexType
1142
+ })}`;
431
1143
  },
432
1144
  url(node) {
433
- return `z.url()${lengthConstraints(node)}`;
1145
+ return `z.url()${lengthConstraints({
1146
+ ...node,
1147
+ regexType: this.options.regexType
1148
+ })}`;
434
1149
  },
435
1150
  ipv4: () => "z.ipv4()",
436
1151
  ipv6: () => "z.ipv6()",
@@ -442,120 +1157,104 @@ const printerZod = ast.definePrinter((options) => {
442
1157
  if (literals.length === 1) return literals[0];
443
1158
  return `z.union([${literals.join(", ")}])`;
444
1159
  }
445
- return `z.enum([${nonNullValues.map(formatLiteral).join(", ")}])`;
1160
+ return buildEnum(nonNullValues);
446
1161
  },
447
1162
  ref(node) {
448
- if (!node.name) return void 0;
449
- const refName = node.ref ? ast.extractRefName(node.ref) ?? node.name : node.name;
450
- const resolvedName = node.ref ? this.options.resolver?.default(refName, "function") ?? refName : node.name;
1163
+ if (!node.name) return null;
1164
+ const refName = ast.resolveRefName(node);
1165
+ if (!refName) return null;
1166
+ const useInputVariant = node.ref != null && this.options.direction === "input" && containsCodec(node);
1167
+ const resolvedName = node.ref ? useInputVariant ? this.options.resolver?.schema.inputName(refName) ?? refName : this.options.resolver?.name(refName) ?? refName : node.name;
451
1168
  if (node.ref && this.options.cyclicSchemas?.has(refName)) return `z.lazy(() => ${resolvedName})`;
452
1169
  return resolvedName;
453
1170
  },
454
1171
  object(node) {
455
- let result = `z.object({\n ${node.properties.map((prop) => {
456
- const { name: propName, schema } = prop;
457
- const meta = ast.syncSchemaRef(schema);
458
- const isNullable = meta.nullable;
459
- const isOptional = schema.optional;
460
- const isNullish = schema.nullish;
461
- const hasSelfRef = this.options.cyclicSchemas != null && ast.containsCircularRef(schema, { circularSchemas: this.options.cyclicSchemas });
462
- if (hasSelfRef) this.options.cyclicSchemas = void 0;
463
- const baseOutput = this.transform(schema) ?? this.transform(ast.createSchema({ type: "unknown" }));
464
- if (hasSelfRef) this.options.cyclicSchemas = options.cyclicSchemas;
465
- const wrappedOutput = this.options.wrapOutput ? this.options.wrapOutput({
466
- output: baseOutput,
467
- schema
468
- }) || baseOutput : baseOutput;
469
- let descriptionToApply = meta.description;
470
- if (schema.type !== "ref" && meta.type === "ref") descriptionToApply = void 0;
471
- const value = applyModifiers({
472
- value: wrappedOutput,
473
- nullable: isNullable,
474
- optional: isOptional,
475
- nullish: isNullish,
476
- defaultValue: meta.default,
477
- description: descriptionToApply
478
- });
479
- if (hasSelfRef) return `get "${propName}"() { return ${value} }`;
480
- return `"${propName}": ${value}`;
481
- }).join(",\n ")}\n })`;
482
- if (node.additionalProperties && node.additionalProperties !== true) {
483
- const catchallType = this.transform(node.additionalProperties);
484
- if (catchallType) result += `.catchall(${catchallType})`;
485
- } else if (node.additionalProperties === true) result += `.catchall(${this.transform(ast.createSchema({ type: "unknown" }))})`;
486
- else if (node.additionalProperties === false) result += ".strict()";
487
- return result;
1172
+ const entries = node.properties ?? [];
1173
+ const objectBase = `z.object(${buildZodObjectShape(this, node)})`;
1174
+ return (() => {
1175
+ const patterns = node.patternProperties ? Object.entries(node.patternProperties) : [];
1176
+ if (node.additionalProperties && node.additionalProperties !== true) {
1177
+ const catchallType = this.transform(node.additionalProperties);
1178
+ return catchallType ? `${objectBase}.catchall(${catchallType})` : objectBase;
1179
+ }
1180
+ if (node.additionalProperties === true) return `${objectBase}.catchall(${this.transform(ast.factory.createSchema({ type: "unknown" }))})`;
1181
+ if (node.additionalProperties === false && patterns.length === 0) return `${objectBase}.strict()`;
1182
+ if (patterns.length > 0) {
1183
+ const values = patterns.map(([, valueSchema]) => {
1184
+ const valueType = this.transform(valueSchema) ?? this.transform(ast.factory.createSchema({ type: "unknown" }));
1185
+ return valueSchema.nullable ? `${valueType}.nullable()` : valueType;
1186
+ });
1187
+ const distinct = [...new Set(values)];
1188
+ const value = distinct.length === 1 ? distinct[0] : `z.union([${distinct.join(", ")}])`;
1189
+ if (entries.length > 0) return `${objectBase}.catchall(${value})`;
1190
+ return `z.record(${patternKeySchema({
1191
+ patterns: patterns.map(([pattern]) => pattern),
1192
+ regexType: this.options.regexType
1193
+ })}, ${value})`;
1194
+ }
1195
+ return objectBase;
1196
+ })();
488
1197
  },
489
1198
  array(node) {
490
- let result = `z.array(${(node.items ?? []).map((item) => this.transform(item)).filter(Boolean).join(", ") || this.transform(ast.createSchema({ type: "unknown" }))})${lengthConstraints(node)}`;
491
- if (node.unique) result += `.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })`;
492
- return result;
1199
+ const base = `z.array(${mapSchemaItems(node, (item) => this.transform(item)).map(({ output }) => output).filter(Boolean).join(", ") || this.transform(ast.factory.createSchema({ type: "unknown" }))})${lengthConstraints({
1200
+ ...node,
1201
+ regexType: this.options.regexType
1202
+ })}`;
1203
+ return node.unique ? `${base}.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })` : base;
493
1204
  },
494
1205
  tuple(node) {
495
- return `z.tuple([${(node.items ?? []).map((item) => this.transform(item)).filter(Boolean).join(", ")}])`;
1206
+ return `z.tuple(${buildList(mapSchemaItems(node, (item) => this.transform(item)).map(({ output }) => output).filter(Boolean))})`;
496
1207
  },
497
1208
  union(node) {
498
1209
  const nodeMembers = node.members ?? [];
499
- const members = nodeMembers.map((memberNode) => {
500
- const member = this.transform(memberNode);
501
- return member && node.strategy === "one" ? strictOneOfMember$1(member, memberNode) : member;
502
- }).filter(Boolean);
1210
+ const members = mapSchemaMembers(node, (memberNode) => this.transform(memberNode)).map(({ schema, output }) => output && node.strategy === "one" ? strictOneOfMember$1(output, schema, cyclicSchemaNames) : output).filter(Boolean);
503
1211
  if (members.length === 0) return "";
504
1212
  if (members.length === 1) return members[0];
505
- if (node.discriminatorPropertyName && !nodeMembers.some((m) => m.type === "intersection")) return `z.discriminatedUnion(${stringify(node.discriminatorPropertyName)}, [${members.join(", ")}])`;
506
- return `z.union([${members.join(", ")}])`;
1213
+ const allDiscriminable = nodeMembers.every((m) => isObjectSchemaNode(m, cyclicSchemaNames));
1214
+ if (node.discriminatorPropertyName && allDiscriminable) return `z.discriminatedUnion(${stringify(node.discriminatorPropertyName)}, ${buildList(members)})`;
1215
+ return `z.union(${buildList(members)})`;
507
1216
  },
508
1217
  intersection(node) {
509
1218
  const members = node.members ?? [];
510
1219
  if (members.length === 0) return "";
511
1220
  const [first, ...rest] = members;
512
1221
  if (!first) return "";
513
- let base = this.transform(first);
514
- if (!base) return "";
515
- for (const member of rest) {
516
- if (member.primitive === "string") {
517
- const c = lengthConstraints(ast.narrowSchema(member, "string") ?? {});
518
- if (c) {
519
- base += c;
520
- continue;
521
- }
522
- } else if (member.primitive === "number" || member.primitive === "integer") {
523
- const c = numberConstraints(ast.narrowSchema(member, "number") ?? ast.narrowSchema(member, "integer") ?? {});
524
- if (c) {
525
- base += c;
526
- continue;
527
- }
528
- } else if (member.primitive === "array") {
529
- const c = lengthConstraints(ast.narrowSchema(member, "array") ?? {});
530
- if (c) {
531
- base += c;
532
- continue;
533
- }
534
- }
1222
+ const firstBase = this.transform(first);
1223
+ if (!firstBase) return "";
1224
+ if (rest.length > 0 && isObjectComposableIntersection(node, cyclicSchemaNames)) return rest.reduce((acc, member) => `${acc}.extend(${buildZodObjectShape(this, member)})`, firstBase);
1225
+ return rest.reduce((acc, member) => {
1226
+ const constraint = getMemberConstraint({
1227
+ member,
1228
+ regexType: this.options.regexType
1229
+ });
1230
+ if (constraint) return acc + constraint;
535
1231
  const transformed = this.transform(member);
536
- if (transformed) base = `${base}.and(${transformed})`;
537
- }
538
- return base;
539
- },
540
- ...options.nodes
1232
+ return transformed ? `${acc}.and(${transformed})` : acc;
1233
+ }, firstBase);
1234
+ }
541
1235
  },
1236
+ overrides: options.nodes,
542
1237
  print(node) {
543
1238
  const { keysToOmit } = this.options;
544
- let base = this.transform(node);
545
- if (!base) return null;
546
- const meta = ast.syncSchemaRef(node);
547
- if (keysToOmit?.length && meta.primitive === "object" && !(meta.type === "union" && meta.discriminatorPropertyName)) {
548
- const lazyMatch = base.match(/^z\.lazy\(\(\)\s*=>\s*(.+)\)$/);
549
- if (lazyMatch) base = `z.lazy(() => ${lazyMatch[1]}.omit({ ${keysToOmit.map((k) => `"${k}": true`).join(", ")} }))`;
550
- else base = `${base}.omit({ ${keysToOmit.map((k) => `"${k}": true`).join(", ")} })`;
551
- }
1239
+ const transformed = this.transform(node);
1240
+ if (!transformed) return null;
1241
+ const meta = syncSchemaRef(node);
552
1242
  return applyModifiers({
553
- value: base,
1243
+ value: (() => {
1244
+ if (!keysToOmit?.length || meta.primitive !== "object" || meta.type === "union" && meta.discriminatorPropertyName) return transformed;
1245
+ const unwrap = omitUnwrapChain(node);
1246
+ const omit = `.omit({ ${keysToOmit.map((k) => `"${k}": true`).join(", ")} })`;
1247
+ const lazyMatch = transformed.match(/^z\.lazy\(\(\)\s*=>\s*(.+)\)$/);
1248
+ if (lazyMatch) return `z.lazy(() => ${lazyMatch[1]}${unwrap}${omit})`;
1249
+ return `${transformed}${unwrap}${omit}`;
1250
+ })(),
1251
+ schema: node,
554
1252
  nullable: meta.nullable,
555
1253
  optional: meta.optional,
556
1254
  nullish: meta.nullish,
557
1255
  defaultValue: meta.default,
558
- description: meta.description
1256
+ description: meta.description,
1257
+ examples: meta.examples
559
1258
  });
560
1259
  }
561
1260
  };
@@ -566,8 +1265,51 @@ function strictOneOfMember(member, node) {
566
1265
  if (node.type === "object" && (node.additionalProperties === void 0 || node.additionalProperties === false)) return member.replace(/^z\.object\(/, "z.strictObject(");
567
1266
  return member;
568
1267
  }
1268
+ function getMemberConstraintMini({ member, regexType }) {
1269
+ if (member.primitive === "string") return lengthChecksMini({
1270
+ ...ast.narrowSchema(member, "string") ?? {},
1271
+ regexType
1272
+ }) || void 0;
1273
+ if (member.primitive === "number" || member.primitive === "integer") return numberChecksMini(ast.narrowSchema(member, "number") ?? ast.narrowSchema(member, "integer") ?? {}) || void 0;
1274
+ if (member.primitive === "array") return lengthChecksMini({
1275
+ ...ast.narrowSchema(member, "array") ?? {},
1276
+ regexType
1277
+ }) || void 0;
1278
+ }
569
1279
  /**
570
- * Zod v4 **Mini** printer built with `definePrinter`.
1280
+ * Builds the `{ key: value, … }` shape for an object node with the functional `zod/mini` modifiers,
1281
+ * shared by the `z.object(...)` and `z.extend(...)` renderings so they stay in lockstep.
1282
+ */
1283
+ function buildZodMiniObjectShape(ctx, node) {
1284
+ const objectNode = ast.narrowSchema(node, "object");
1285
+ if (!objectNode) return "{}";
1286
+ const isCyclic = (schema) => ctx.options.cyclicSchemas != null && containsCircularRef(schema, { circularSchemas: ctx.options.cyclicSchemas });
1287
+ return buildObject(mapSchemaProperties(objectNode, (schema) => {
1288
+ const hasSelfRef = isCyclic(schema);
1289
+ const savedCyclicSchemas = ctx.options.cyclicSchemas;
1290
+ if (hasSelfRef) ctx.options.cyclicSchemas = void 0;
1291
+ const baseOutput = ctx.transform(schema) ?? ctx.transform(ast.factory.createSchema({ type: "unknown" }));
1292
+ if (hasSelfRef) ctx.options.cyclicSchemas = savedCyclicSchemas;
1293
+ return baseOutput;
1294
+ }).map(({ name: propName, property, output: baseOutput }) => {
1295
+ const { schema } = property;
1296
+ const meta = syncSchemaRef(schema);
1297
+ const value = applyMiniModifiers({
1298
+ value: baseOutput,
1299
+ schema,
1300
+ nullable: meta.nullable,
1301
+ optional: schema.optional || property.required === false,
1302
+ nullish: schema.nullish,
1303
+ defaultValue: meta.default
1304
+ });
1305
+ return isCyclic(schema) ? lazyGetter({
1306
+ name: propName,
1307
+ body: value
1308
+ }) : `${objectKey(propName)}: ${value}`;
1309
+ }));
1310
+ }
1311
+ /**
1312
+ * Zod v4 Mini printer built with `definePrinter`.
571
1313
  *
572
1314
  * Converts a `SchemaNode` AST into a Zod v4 code string using the functional API
573
1315
  * (`z.optional(z.string())`) for improved tree-shaking. See {@link printerZod} for the chainable API.
@@ -578,7 +1320,8 @@ function strictOneOfMember(member, node) {
578
1320
  * const code = printer.print(optionalStringNode) // "z.optional(z.string())"
579
1321
  * ```
580
1322
  */
581
- const printerZodMini = ast.definePrinter((options) => {
1323
+ const printerZodMini = ast.createPrinter((options) => {
1324
+ const cyclicSchemaNames = options.cyclicSchemas;
582
1325
  return {
583
1326
  name: "zod-mini",
584
1327
  options,
@@ -590,7 +1333,10 @@ const printerZodMini = ast.definePrinter((options) => {
590
1333
  boolean: () => "z.boolean()",
591
1334
  null: () => "z.null()",
592
1335
  string(node) {
593
- return `z.string()${lengthChecksMini(node)}`;
1336
+ return `z.string()${lengthChecksMini({
1337
+ ...node,
1338
+ regexType: this.options.regexType
1339
+ })}`;
594
1340
  },
595
1341
  number(node) {
596
1342
  return `z.number()${numberChecksMini(node)}`;
@@ -613,13 +1359,22 @@ const printerZodMini = ast.definePrinter((options) => {
613
1359
  return "z.date()";
614
1360
  },
615
1361
  uuid(node) {
616
- return `${this.options.guidType === "guid" ? "z.guid()" : "z.uuid()"}${lengthChecksMini(node)}`;
1362
+ return `${this.options.guidType === "guid" ? "z.guid()" : "z.uuid()"}${lengthChecksMini({
1363
+ ...node,
1364
+ regexType: this.options.regexType
1365
+ })}`;
617
1366
  },
618
1367
  email(node) {
619
- return `z.email()${lengthChecksMini(node)}`;
1368
+ return `z.email()${lengthChecksMini({
1369
+ ...node,
1370
+ regexType: this.options.regexType
1371
+ })}`;
620
1372
  },
621
1373
  url(node) {
622
- return `z.url()${lengthChecksMini(node)}`;
1374
+ return `z.url()${lengthChecksMini({
1375
+ ...node,
1376
+ regexType: this.options.regexType
1377
+ })}`;
623
1378
  },
624
1379
  ipv4: () => "z.ipv4()",
625
1380
  ipv6: () => "z.ipv6()",
@@ -631,105 +1386,95 @@ const printerZodMini = ast.definePrinter((options) => {
631
1386
  if (literals.length === 1) return literals[0];
632
1387
  return `z.union([${literals.join(", ")}])`;
633
1388
  }
634
- return `z.enum([${nonNullValues.map(formatLiteral).join(", ")}])`;
1389
+ return buildEnum(nonNullValues);
635
1390
  },
636
1391
  ref(node) {
637
- if (!node.name) return void 0;
638
- const refName = node.ref ? ast.extractRefName(node.ref) ?? node.name : node.name;
639
- const resolvedName = node.ref ? this.options.resolver?.default(refName, "function") ?? refName : node.name;
1392
+ if (!node.name) return null;
1393
+ const refName = ast.resolveRefName(node);
1394
+ if (!refName) return null;
1395
+ const resolvedName = node.ref ? this.options.resolver?.name(refName) ?? refName : node.name;
640
1396
  if (node.ref && this.options.cyclicSchemas?.has(refName)) return `z.lazy(() => ${resolvedName})`;
641
1397
  return resolvedName;
642
1398
  },
643
1399
  object(node) {
644
- return `z.object({\n ${node.properties.map((prop) => {
645
- const { name: propName, schema } = prop;
646
- const meta = ast.syncSchemaRef(schema);
647
- const isNullable = meta.nullable;
648
- const isOptional = schema.optional;
649
- const isNullish = schema.nullish;
650
- const hasSelfRef = this.options.cyclicSchemas != null && ast.containsCircularRef(schema, { circularSchemas: this.options.cyclicSchemas });
651
- if (hasSelfRef) this.options.cyclicSchemas = void 0;
652
- const baseOutput = this.transform(schema) ?? this.transform(ast.createSchema({ type: "unknown" }));
653
- if (hasSelfRef) this.options.cyclicSchemas = options.cyclicSchemas;
654
- const value = applyMiniModifiers({
655
- value: this.options.wrapOutput ? this.options.wrapOutput({
656
- output: baseOutput,
657
- schema
658
- }) || baseOutput : baseOutput,
659
- nullable: isNullable,
660
- optional: isOptional,
661
- nullish: isNullish,
662
- defaultValue: meta.default
1400
+ const entries = node.properties ?? [];
1401
+ const objectBase = `z.object(${buildZodMiniObjectShape(this, node)})`;
1402
+ const patterns = node.patternProperties ? Object.entries(node.patternProperties) : [];
1403
+ if (node.additionalProperties && node.additionalProperties !== true) {
1404
+ const catchallType = this.transform(node.additionalProperties);
1405
+ return catchallType ? `z.catchall(${objectBase}, ${catchallType})` : objectBase;
1406
+ }
1407
+ if (node.additionalProperties === true) return `z.catchall(${objectBase}, ${this.transform(ast.factory.createSchema({ type: "unknown" }))})`;
1408
+ if (node.additionalProperties === false && patterns.length === 0) return objectBase.replace(/^z\.object\(/, "z.strictObject(");
1409
+ if (patterns.length > 0) {
1410
+ const values = patterns.map(([, valueSchema]) => {
1411
+ const valueType = this.transform(valueSchema) ?? this.transform(ast.factory.createSchema({ type: "unknown" }));
1412
+ return valueSchema.nullable ? `z.nullable(${valueType})` : valueType;
663
1413
  });
664
- if (hasSelfRef) return `get "${propName}"() { return ${value} }`;
665
- return `"${propName}": ${value}`;
666
- }).join(",\n ")}\n })`;
1414
+ const distinct = [...new Set(values)];
1415
+ const value = distinct.length === 1 ? distinct[0] : `z.union([${distinct.join(", ")}])`;
1416
+ if (entries.length > 0) return `z.catchall(${objectBase}, ${value})`;
1417
+ return `z.record(${patternKeySchemaMini({
1418
+ patterns: patterns.map(([pattern]) => pattern),
1419
+ regexType: this.options.regexType
1420
+ })}, ${value})`;
1421
+ }
1422
+ return objectBase;
667
1423
  },
668
1424
  array(node) {
669
- let result = `z.array(${(node.items ?? []).map((item) => this.transform(item)).filter(Boolean).join(", ") || this.transform(ast.createSchema({ type: "unknown" }))})${lengthChecksMini(node)}`;
670
- if (node.unique) result += `.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })`;
671
- return result;
1425
+ const base = `z.array(${mapSchemaItems(node, (item) => this.transform(item)).map(({ output }) => output).filter(Boolean).join(", ") || this.transform(ast.factory.createSchema({ type: "unknown" }))})${lengthChecksMini({
1426
+ ...node,
1427
+ regexType: this.options.regexType
1428
+ })}`;
1429
+ return node.unique ? `${base}.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })` : base;
672
1430
  },
673
1431
  tuple(node) {
674
- return `z.tuple([${(node.items ?? []).map((item) => this.transform(item)).filter(Boolean).join(", ")}])`;
1432
+ return `z.tuple(${buildList(mapSchemaItems(node, (item) => this.transform(item)).map(({ output }) => output).filter(Boolean))})`;
675
1433
  },
676
1434
  union(node) {
677
1435
  const nodeMembers = node.members ?? [];
678
- const members = nodeMembers.map((memberNode) => {
679
- const member = this.transform(memberNode);
680
- return member && node.strategy === "one" ? strictOneOfMember(member, memberNode) : member;
681
- }).filter(Boolean);
1436
+ const members = mapSchemaMembers(node, (memberNode) => this.transform(memberNode)).map(({ schema, output }) => output && node.strategy === "one" ? strictOneOfMember(output, schema) : output).filter(Boolean);
682
1437
  if (members.length === 0) return "";
683
1438
  if (members.length === 1) return members[0];
684
- if (node.discriminatorPropertyName && !nodeMembers.some((m) => m.type === "intersection")) return `z.discriminatedUnion(${stringify(node.discriminatorPropertyName)}, [${members.join(", ")}])`;
685
- return `z.union([${members.join(", ")}])`;
1439
+ const allDiscriminable = nodeMembers.every((m) => isObjectSchemaNode(m, cyclicSchemaNames));
1440
+ if (node.discriminatorPropertyName && allDiscriminable) return `z.discriminatedUnion(${stringify(node.discriminatorPropertyName)}, ${buildList(members)})`;
1441
+ return `z.union(${buildList(members)})`;
686
1442
  },
687
1443
  intersection(node) {
688
1444
  const members = node.members ?? [];
689
1445
  if (members.length === 0) return "";
690
1446
  const [first, ...rest] = members;
691
1447
  if (!first) return "";
692
- let base = this.transform(first);
693
- if (!base) return "";
694
- for (const member of rest) {
695
- if (member.primitive === "string") {
696
- const c = lengthChecksMini(ast.narrowSchema(member, "string") ?? {});
697
- if (c) {
698
- base += c;
699
- continue;
700
- }
701
- } else if (member.primitive === "number" || member.primitive === "integer") {
702
- const c = numberChecksMini(ast.narrowSchema(member, "number") ?? ast.narrowSchema(member, "integer") ?? {});
703
- if (c) {
704
- base += c;
705
- continue;
706
- }
707
- } else if (member.primitive === "array") {
708
- const c = lengthChecksMini(ast.narrowSchema(member, "array") ?? {});
709
- if (c) {
710
- base += c;
711
- continue;
712
- }
713
- }
1448
+ const firstBase = this.transform(first);
1449
+ if (!firstBase) return "";
1450
+ if (rest.length > 0 && isObjectComposableIntersection(node, cyclicSchemaNames)) return rest.reduce((acc, member) => `z.extend(${acc}, ${buildZodMiniObjectShape(this, member)})`, firstBase);
1451
+ return rest.reduce((acc, member) => {
1452
+ const constraint = getMemberConstraintMini({
1453
+ member,
1454
+ regexType: this.options.regexType
1455
+ });
1456
+ if (constraint) return acc + constraint;
714
1457
  const transformed = this.transform(member);
715
- if (transformed) base = `z.intersection(${base}, ${transformed})`;
716
- }
717
- return base;
718
- },
719
- ...options.nodes
1458
+ return transformed ? `z.intersection(${acc}, ${transformed})` : acc;
1459
+ }, firstBase);
1460
+ }
720
1461
  },
1462
+ overrides: options.nodes,
721
1463
  print(node) {
722
1464
  const { keysToOmit } = this.options;
723
- let base = this.transform(node);
724
- if (!base) return null;
725
- const meta = ast.syncSchemaRef(node);
726
- if (keysToOmit?.length && meta.primitive === "object" && !(meta.type === "union" && meta.discriminatorPropertyName)) {
727
- const lazyMatch = base.match(/^z\.lazy\(\(\)\s*=>\s*(.+)\)$/);
728
- if (lazyMatch) base = `z.lazy(() => ${lazyMatch[1]}.omit({ ${keysToOmit.map((k) => `"${k}": true`).join(", ")} }))`;
729
- else base = `${base}.omit({ ${keysToOmit.map((k) => `"${k}": true`).join(", ")} })`;
730
- }
1465
+ const transformed = this.transform(node);
1466
+ if (!transformed) return null;
1467
+ const meta = syncSchemaRef(node);
731
1468
  return applyMiniModifiers({
732
- value: base,
1469
+ value: (() => {
1470
+ if (!keysToOmit?.length || meta.primitive !== "object" || meta.type === "union" && meta.discriminatorPropertyName) return transformed;
1471
+ const unwrap = omitUnwrapChain(node);
1472
+ const omit = `.omit({ ${keysToOmit.map((k) => `"${k}": true`).join(", ")} })`;
1473
+ const lazyMatch = transformed.match(/^z\.lazy\(\(\)\s*=>\s*(.+)\)$/);
1474
+ if (lazyMatch) return `z.lazy(() => ${lazyMatch[1]}${unwrap}${omit})`;
1475
+ return `${transformed}${unwrap}${omit}`;
1476
+ })(),
1477
+ schema: node,
733
1478
  nullable: meta.nullable,
734
1479
  optional: meta.optional,
735
1480
  nullish: meta.nullish,
@@ -740,66 +1485,143 @@ const printerZodMini = ast.definePrinter((options) => {
740
1485
  });
741
1486
  //#endregion
742
1487
  //#region src/generators/zodGenerator.tsx
1488
+ const zodPrinterCache = /* @__PURE__ */ new WeakMap();
1489
+ const zodMiniPrinterCache = /* @__PURE__ */ new WeakMap();
1490
+ /**
1491
+ * Returns the cached `output`/`input` direction printers for a resolver, building them on
1492
+ * first use. The `input` printer encodes `Date → string` for request bodies, and `output` decodes
1493
+ * `string → Date` for responses. Schemas without `dateType: 'date'` fields print identically.
1494
+ */
1495
+ function getStdPrinters(resolver, params) {
1496
+ const cached = zodPrinterCache.get(resolver);
1497
+ if (cached && cached.coercion === params.coercion && cached.guidType === params.guidType && cached.regexType === params.regexType && cached.dateType === params.dateType && cached.nodes === params.nodes) return {
1498
+ output: cached.output,
1499
+ input: cached.input
1500
+ };
1501
+ const output = printerZod({
1502
+ ...params,
1503
+ resolver,
1504
+ direction: "output"
1505
+ });
1506
+ const input = printerZod({
1507
+ ...params,
1508
+ resolver,
1509
+ direction: "input"
1510
+ });
1511
+ zodPrinterCache.set(resolver, {
1512
+ output,
1513
+ input,
1514
+ coercion: params.coercion,
1515
+ guidType: params.guidType,
1516
+ regexType: params.regexType,
1517
+ dateType: params.dateType,
1518
+ nodes: params.nodes
1519
+ });
1520
+ return {
1521
+ output,
1522
+ input
1523
+ };
1524
+ }
1525
+ function getMiniPrinter(resolver, params) {
1526
+ const cached = zodMiniPrinterCache.get(resolver);
1527
+ if (cached && cached.guidType === params.guidType && cached.regexType === params.regexType && cached.nodes === params.nodes) return cached.printer;
1528
+ const p = printerZodMini({
1529
+ ...params,
1530
+ resolver
1531
+ });
1532
+ zodMiniPrinterCache.set(resolver, {
1533
+ printer: p,
1534
+ guidType: params.guidType,
1535
+ regexType: params.regexType,
1536
+ nodes: params.nodes
1537
+ });
1538
+ return p;
1539
+ }
1540
+ /**
1541
+ * Built-in generator for `@kubb/plugin-zod`. Emits one Zod schema per
1542
+ * schema in the spec plus per-operation request/response/parameter schemas.
1543
+ * When `mini: true`, schemas use the Zod Mini functional API instead of
1544
+ * chainable methods.
1545
+ */
743
1546
  const zodGenerator = defineGenerator({
744
1547
  name: "zod",
745
1548
  renderer: jsxRenderer,
746
1549
  schema(node, ctx) {
747
1550
  const { adapter, config, resolver, root } = ctx;
748
- const { output, coercion, guidType, mini, wrapOutput, inferred, importPath, group, printer } = ctx.options;
749
- const dateType = adapter.options.dateType;
1551
+ const { output, coercion, guidType, regexType, mini, inferred, importPath, group, printer } = ctx.options;
1552
+ const dateType = getOasAdapter(adapter).options.dateType;
750
1553
  if (!node.name) return;
751
- const mode = ctx.getMode(output);
752
1554
  const isZodImport = ZOD_NAMESPACE_IMPORTS.has(importPath);
753
- const imports = adapter.getImports(node, (schemaName) => ({
754
- name: resolver.resolveSchemaName(schemaName),
755
- path: resolver.resolveFile({
1555
+ const cyclicSchemas = new Set(ctx.meta.circularNames);
1556
+ const hasCodec = !mini && containsCodec(node);
1557
+ const codecRefNames = new Set(hasCodec ? collectCodecRefNames(node) : []);
1558
+ const importEntries = resolver.imports({
1559
+ node,
1560
+ root,
1561
+ output,
1562
+ group: group ?? void 0
1563
+ });
1564
+ const inputImportEntries = hasCodec ? [...codecRefNames].map((schemaName) => ({
1565
+ name: [resolver.schema.inputName(schemaName)],
1566
+ path: resolver.file({
756
1567
  name: schemaName,
757
- extname: ".ts"
758
- }, {
1568
+ extname: ".ts",
759
1569
  root,
760
1570
  output,
761
- group
1571
+ group: group ?? void 0
762
1572
  }).path
763
- }));
1573
+ })) : [];
1574
+ const seenImports = /* @__PURE__ */ new Set();
1575
+ const imports = [...importEntries, ...inputImportEntries].filter((imp) => {
1576
+ const key = `${Array.isArray(imp.name) ? imp.name.join(",") : imp.name}|${imp.path}`;
1577
+ if (seenImports.has(key)) return false;
1578
+ seenImports.add(key);
1579
+ return true;
1580
+ });
764
1581
  const meta = {
765
- name: resolver.resolveSchemaName(node.name),
766
- file: resolver.resolveFile({
1582
+ name: resolver.name(node.name),
1583
+ file: resolver.file({
767
1584
  name: node.name,
768
- extname: ".ts"
769
- }, {
1585
+ extname: ".ts",
770
1586
  root,
771
1587
  output,
772
- group
1588
+ group: group ?? void 0
773
1589
  })
774
1590
  };
775
- const inferTypeName = inferred ? resolver.resolveSchemaTypeName(node.name) : void 0;
776
- const cyclicSchemas = adapter.inputNode ? ast.findCircularSchemas(adapter.inputNode.schemas) : void 0;
777
- const schemaPrinter = mini ? printerZodMini({
778
- guidType,
779
- wrapOutput,
780
- resolver,
781
- cyclicSchemas,
782
- nodes: printer?.nodes
783
- }) : printerZod({
1591
+ const inferTypeName = inferred ? resolver.schema.typeName(node.name) : null;
1592
+ const stdPrinters = mini ? null : getStdPrinters(resolver, {
784
1593
  coercion,
785
1594
  guidType,
1595
+ regexType,
786
1596
  dateType,
787
- wrapOutput,
788
- resolver,
789
1597
  cyclicSchemas,
790
1598
  nodes: printer?.nodes
791
1599
  });
1600
+ const schemaPrinter = mini ? getMiniPrinter(resolver, {
1601
+ guidType,
1602
+ regexType,
1603
+ cyclicSchemas,
1604
+ nodes: printer?.nodes
1605
+ }) : stdPrinters.output;
792
1606
  return /* @__PURE__ */ jsxs(File, {
793
1607
  baseName: meta.file.baseName,
794
1608
  path: meta.file.path,
795
1609
  meta: meta.file.meta,
796
- banner: resolver.resolveBanner(adapter.inputNode, {
1610
+ banner: resolver.default.banner(ctx.meta, {
797
1611
  output,
798
- config
1612
+ config,
1613
+ file: {
1614
+ path: meta.file.path,
1615
+ baseName: meta.file.baseName
1616
+ }
799
1617
  }),
800
- footer: resolver.resolveFooter(adapter.inputNode, {
1618
+ footer: resolver.default.footer(ctx.meta, {
801
1619
  output,
802
- config
1620
+ config,
1621
+ file: {
1622
+ path: meta.file.path,
1623
+ baseName: meta.file.baseName
1624
+ }
803
1625
  }),
804
1626
  children: [
805
1627
  /* @__PURE__ */ jsx(File.Import, {
@@ -807,70 +1629,90 @@ const zodGenerator = defineGenerator({
807
1629
  path: importPath,
808
1630
  isNameSpace: isZodImport
809
1631
  }),
810
- mode === "split" && imports.map((imp) => /* @__PURE__ */ jsx(File.Import, {
1632
+ imports.map((imp) => /* @__PURE__ */ jsx(File.Import, {
811
1633
  root: meta.file.path,
812
1634
  path: imp.path,
813
1635
  name: imp.name
814
- }, [node.name, imp.path].join("-"))),
1636
+ }, [
1637
+ node.name,
1638
+ imp.path,
1639
+ imp.name
1640
+ ].join("-"))),
815
1641
  /* @__PURE__ */ jsx(Zod, {
816
1642
  name: meta.name,
817
1643
  node,
818
1644
  printer: schemaPrinter,
819
- inferTypeName
1645
+ inferTypeName,
1646
+ cyclic: cyclicSchemas.has(node.name)
1647
+ }),
1648
+ hasCodec && stdPrinters && /* @__PURE__ */ jsx(Zod, {
1649
+ name: resolver.schema.inputName(node.name),
1650
+ node,
1651
+ printer: stdPrinters.input,
1652
+ inferTypeName: inferred ? resolver.schema.inputTypeName(node.name) : null,
1653
+ cyclic: cyclicSchemas.has(node.name)
820
1654
  })
821
1655
  ]
822
1656
  });
823
1657
  },
824
1658
  operation(node, ctx) {
1659
+ if (!ast.isHttpOperationNode(node)) return null;
825
1660
  const { adapter, config, resolver, root } = ctx;
826
- const { output, coercion, guidType, mini, wrapOutput, inferred, importPath, group, paramsCasing, printer } = ctx.options;
827
- const dateType = adapter.options.dateType;
828
- const mode = ctx.getMode(output);
1661
+ const { output, coercion, guidType, regexType, mini, inferred, importPath, group, printer } = ctx.options;
1662
+ const dateType = getOasAdapter(adapter).options.dateType;
829
1663
  const isZodImport = ZOD_NAMESPACE_IMPORTS.has(importPath);
830
- const params = ast.caseParams(node.parameters, paramsCasing);
831
- const meta = { file: resolver.resolveFile({
1664
+ const meta = { file: resolver.file({
832
1665
  name: node.operationId,
833
1666
  extname: ".ts",
834
1667
  tag: node.tags[0] ?? "default",
835
- path: node.path
836
- }, {
1668
+ path: node.path,
837
1669
  root,
838
1670
  output,
839
- group
1671
+ group: group ?? void 0
840
1672
  }) };
841
- const cyclicSchemas = adapter.inputNode ? ast.findCircularSchemas(adapter.inputNode.schemas) : void 0;
842
- function renderSchemaEntry({ schema, name, keysToOmit }) {
1673
+ const cyclicSchemas = new Set(ctx.meta.circularNames);
1674
+ function renderSchemaEntry({ schema, name, keysToOmit, direction = "output" }) {
843
1675
  if (!schema) return null;
844
- const inferTypeName = inferred ? resolver.resolveTypeName(name) : void 0;
845
- const imports = adapter.getImports(schema, (schemaName) => ({
846
- name: resolver.resolveSchemaName(schemaName),
847
- path: resolver.resolveFile({
848
- name: schemaName,
849
- extname: ".ts"
850
- }, {
851
- root,
852
- output,
853
- group
854
- }).path
855
- }));
856
- const schemaPrinter = mini ? printerZodMini({
1676
+ const inferTypeName = inferred ? resolver.schema.type(name) : null;
1677
+ const codecRefNames = direction === "input" && !mini ? new Set(collectCodecRefNames(schema)) : null;
1678
+ const imports = resolver.imports({
1679
+ node: schema,
1680
+ root,
1681
+ output,
1682
+ group: group ?? void 0,
1683
+ name: (schemaName) => codecRefNames?.has(schemaName) ? resolver.schema.inputName(schemaName) : resolver.name(schemaName)
1684
+ });
1685
+ const schemaPrinter = mini ? keysToOmit?.length ? printerZodMini({
857
1686
  guidType,
858
- wrapOutput,
1687
+ regexType,
859
1688
  resolver,
860
1689
  keysToOmit,
861
1690
  cyclicSchemas,
862
1691
  nodes: printer?.nodes
863
- }) : printerZod({
1692
+ }) : getMiniPrinter(resolver, {
1693
+ guidType,
1694
+ regexType,
1695
+ cyclicSchemas,
1696
+ nodes: printer?.nodes
1697
+ }) : keysToOmit?.length ? printerZod({
864
1698
  coercion,
865
1699
  guidType,
1700
+ regexType,
866
1701
  dateType,
867
- wrapOutput,
868
1702
  resolver,
869
1703
  keysToOmit,
870
1704
  cyclicSchemas,
1705
+ nodes: printer?.nodes,
1706
+ direction
1707
+ }) : getStdPrinters(resolver, {
1708
+ coercion,
1709
+ guidType,
1710
+ regexType,
1711
+ dateType,
1712
+ cyclicSchemas,
871
1713
  nodes: printer?.nodes
872
- });
873
- return /* @__PURE__ */ jsxs(Fragment, { children: [mode === "split" && imports.map((imp) => /* @__PURE__ */ jsx(File.Import, {
1714
+ })[direction];
1715
+ return /* @__PURE__ */ jsxs(Fragment, { children: [imports.map((imp) => /* @__PURE__ */ jsx(File.Import, {
874
1716
  root: meta.file.path,
875
1717
  path: imp.path,
876
1718
  name: imp.name
@@ -882,56 +1724,140 @@ const zodGenerator = defineGenerator({
882
1724
  name,
883
1725
  node: schema,
884
1726
  printer: schemaPrinter,
885
- inferTypeName
1727
+ inferTypeName,
1728
+ cyclic: false
886
1729
  })] });
887
1730
  }
888
- const paramSchemas = params.map((param) => renderSchemaEntry({
889
- schema: param.schema,
890
- name: resolver.resolveParamName(node, param)
891
- }));
892
- const responseSchemas = node.responses.map((res) => renderSchemaEntry({
893
- schema: res.schema,
894
- name: resolver.resolveResponseStatusName(node, res.statusCode),
895
- keysToOmit: res.keysToOmit
896
- }));
897
- const responsesWithSchema = node.responses.filter((res) => res.schema);
898
- const responseUnionSchema = responsesWithSchema.length > 0 ? (() => {
899
- const responseUnionName = resolver.resolveResponseName(node);
900
- if (new Set(responsesWithSchema.flatMap((res) => res.schema ? adapter.getImports(res.schema, (schemaName) => ({
901
- name: resolver.resolveSchemaName(schemaName),
902
- path: ""
903
- })).flatMap((imp) => Array.isArray(imp.name) ? imp.name : [imp.name]) : [])).has(responseUnionName)) return null;
904
- const members = responsesWithSchema.map((res) => ast.createSchema({
1731
+ function buildContentTypeVariants(entries, baseName, decorate, direction) {
1732
+ const variants = resolveContentTypeVariants(entries, baseName);
1733
+ const unionSchema = ast.factory.createSchema({
1734
+ type: "union",
1735
+ members: variants.map((variant) => ast.factory.createSchema({
1736
+ type: "ref",
1737
+ name: variant.name
1738
+ }))
1739
+ });
1740
+ return /* @__PURE__ */ jsxs(Fragment, { children: [variants.map((variant) => renderSchemaEntry({
1741
+ schema: decorate ? decorate(variant.schema) : variant.schema,
1742
+ name: variant.name,
1743
+ keysToOmit: variant.keysToOmit,
1744
+ direction
1745
+ })), renderSchemaEntry({
1746
+ schema: unionSchema,
1747
+ name: baseName,
1748
+ direction
1749
+ })] });
1750
+ }
1751
+ function buildResponseUnion({ responses, name, fallbackUnknown }) {
1752
+ if (new Set(responses.flatMap((res) => (res.content ?? []).flatMap((entry) => entry.schema ? collectRefNames(entry.schema).map((refName) => resolver.name(refName)) : []))).has(name)) return null;
1753
+ const members = responses.map((res) => ast.factory.createSchema({
905
1754
  type: "ref",
906
- name: resolver.resolveResponseStatusName(node, res.statusCode)
1755
+ name: resolver.response.status(node, res.statusCode)
907
1756
  }));
1757
+ if (fallbackUnknown && members.length === 0) return renderSchemaEntry({
1758
+ schema: ast.factory.createSchema({ type: "unknown" }),
1759
+ name
1760
+ });
908
1761
  return renderSchemaEntry({
909
- schema: members.length === 1 ? members[0] : ast.createSchema({
1762
+ schema: members.length === 1 ? members[0] : ast.factory.createSchema({
910
1763
  type: "union",
911
1764
  members
912
1765
  }),
913
- name: responseUnionName
1766
+ name
1767
+ });
1768
+ }
1769
+ const paramSchemas = node.parameters.map((param) => renderSchemaEntry({
1770
+ schema: param.schema,
1771
+ name: resolver.param.name(node, param),
1772
+ direction: "input"
1773
+ }));
1774
+ const responseSchemas = node.responses.map((res) => {
1775
+ const variants = (res.content ?? []).filter((entry) => entry.schema);
1776
+ if (variants.length > 1) return buildContentTypeVariants(res.content, resolver.response.status(node, res.statusCode));
1777
+ const primary = variants[0] ?? res.content?.[0];
1778
+ return renderSchemaEntry({
1779
+ schema: primary?.schema ?? null,
1780
+ name: resolver.response.status(node, res.statusCode),
1781
+ keysToOmit: primary?.keysToOmit
914
1782
  });
915
- })() : null;
916
- const requestSchema = node.requestBody?.content?.[0]?.schema ? renderSchemaEntry({
917
- schema: {
918
- ...node.requestBody.content[0].schema,
919
- description: node.requestBody.description ?? node.requestBody.content[0].schema.description
1783
+ });
1784
+ const responsesWithSchema = node.responses.filter((res) => res.content?.some((entry) => entry.schema));
1785
+ const successResponsesWithSchema = getSuccessResponses(responsesWithSchema);
1786
+ const responseUnionSchema = responsesWithSchema.length > 0 ? buildResponseUnion({
1787
+ responses: successResponsesWithSchema,
1788
+ name: resolver.response.response(node),
1789
+ fallbackUnknown: true
1790
+ }) : null;
1791
+ const errorResponsesWithSchema = responsesWithSchema.filter((res) => !isSuccessStatusCode(res.statusCode));
1792
+ const errorUnionSchema = errorResponsesWithSchema.length > 0 ? buildResponseUnion({
1793
+ responses: errorResponsesWithSchema,
1794
+ name: resolver.response.error(node),
1795
+ fallbackUnknown: false
1796
+ }) : null;
1797
+ const requestBodyContent = node.requestBody?.content ?? [];
1798
+ const requestSchema = (() => {
1799
+ if (requestBodyContent.length === 0) return null;
1800
+ if (requestBodyContent.length === 1) {
1801
+ const entry = requestBodyContent[0];
1802
+ if (!entry.schema) return null;
1803
+ return renderSchemaEntry({
1804
+ schema: {
1805
+ ...entry.schema,
1806
+ description: node.requestBody.description ?? entry.schema.description
1807
+ },
1808
+ name: resolver.response.body(node),
1809
+ keysToOmit: entry.keysToOmit,
1810
+ direction: "input"
1811
+ });
1812
+ }
1813
+ return buildContentTypeVariants(requestBodyContent, resolver.response.body(node), (schema) => ({
1814
+ ...schema,
1815
+ description: node.requestBody.description ?? schema.description
1816
+ }), "input");
1817
+ })();
1818
+ const { path, query, header } = getOperationParameters(node);
1819
+ const paramGroupSchemas = inferred ? [
1820
+ {
1821
+ kind: "path",
1822
+ params: path
1823
+ },
1824
+ {
1825
+ kind: "query",
1826
+ params: query
920
1827
  },
921
- name: resolver.resolveDataName(node),
922
- keysToOmit: node.requestBody.content[0].keysToOmit
1828
+ {
1829
+ kind: "headers",
1830
+ params: header
1831
+ }
1832
+ ].filter(({ params }) => params.length > 0).map(({ kind, params }) => renderSchemaEntry({
1833
+ schema: buildGroupedParamsSchema({ params }),
1834
+ name: resolver.param[kind](node, params[0]),
1835
+ direction: "input"
1836
+ })) : [];
1837
+ const optionsSchema = inferred ? renderSchemaEntry({
1838
+ schema: buildOptionsSchema(node, resolver),
1839
+ name: resolver.name(`${node.operationId} Options`),
1840
+ direction: "input"
923
1841
  }) : null;
924
1842
  return /* @__PURE__ */ jsxs(File, {
925
1843
  baseName: meta.file.baseName,
926
1844
  path: meta.file.path,
927
1845
  meta: meta.file.meta,
928
- banner: resolver.resolveBanner(adapter.inputNode, {
1846
+ banner: resolver.default.banner(ctx.meta, {
929
1847
  output,
930
- config
1848
+ config,
1849
+ file: {
1850
+ path: meta.file.path,
1851
+ baseName: meta.file.baseName
1852
+ }
931
1853
  }),
932
- footer: resolver.resolveFooter(adapter.inputNode, {
1854
+ footer: resolver.default.footer(ctx.meta, {
933
1855
  output,
934
- config
1856
+ config,
1857
+ file: {
1858
+ path: meta.file.path,
1859
+ baseName: meta.file.baseName
1860
+ }
935
1861
  }),
936
1862
  children: [
937
1863
  /* @__PURE__ */ jsx(File.Import, {
@@ -942,78 +1868,10 @@ const zodGenerator = defineGenerator({
942
1868
  paramSchemas,
943
1869
  responseSchemas,
944
1870
  responseUnionSchema,
945
- requestSchema
946
- ]
947
- });
948
- },
949
- operations(nodes, ctx) {
950
- const { adapter, config, resolver, root } = ctx;
951
- const { output, importPath, group, operations, paramsCasing } = ctx.options;
952
- if (!operations) return;
953
- const isZodImport = ZOD_NAMESPACE_IMPORTS.has(importPath);
954
- const meta = { file: resolver.resolveFile({
955
- name: "operations",
956
- extname: ".ts"
957
- }, {
958
- root,
959
- output,
960
- group
961
- }) };
962
- const transformedOperations = nodes.map((node) => {
963
- return {
964
- node,
965
- data: buildSchemaNames(node, {
966
- params: ast.caseParams(node.parameters, paramsCasing),
967
- resolver
968
- })
969
- };
970
- });
971
- const imports = transformedOperations.flatMap(({ node, data }) => {
972
- const names = [
973
- data.request,
974
- ...Object.values(data.responses),
975
- ...Object.values(data.parameters)
976
- ].filter(Boolean);
977
- const opFile = resolver.resolveFile({
978
- name: node.operationId,
979
- extname: ".ts",
980
- tag: node.tags[0] ?? "default",
981
- path: node.path
982
- }, {
983
- root,
984
- output,
985
- group
986
- });
987
- return names.map((name) => /* @__PURE__ */ jsx(File.Import, {
988
- name: [name],
989
- root: meta.file.path,
990
- path: opFile.path
991
- }, [name, opFile.path].join("-")));
992
- });
993
- return /* @__PURE__ */ jsxs(File, {
994
- baseName: meta.file.baseName,
995
- path: meta.file.path,
996
- meta: meta.file.meta,
997
- banner: resolver.resolveBanner(adapter.inputNode, {
998
- output,
999
- config
1000
- }),
1001
- footer: resolver.resolveFooter(adapter.inputNode, {
1002
- output,
1003
- config
1004
- }),
1005
- children: [
1006
- /* @__PURE__ */ jsx(File.Import, {
1007
- isTypeOnly: true,
1008
- name: isZodImport ? "z" : ["z"],
1009
- path: importPath,
1010
- isNameSpace: isZodImport
1011
- }),
1012
- imports,
1013
- /* @__PURE__ */ jsx(Operations, {
1014
- name: "operations",
1015
- operations: transformedOperations
1016
- })
1871
+ errorUnionSchema,
1872
+ requestSchema,
1873
+ paramGroupSchemas,
1874
+ optionsSchema
1017
1875
  ]
1018
1876
  });
1019
1877
  }
@@ -1021,92 +1879,90 @@ const zodGenerator = defineGenerator({
1021
1879
  //#endregion
1022
1880
  //#region src/resolvers/resolverZod.ts
1023
1881
  /**
1024
- * Naming convention resolver for Zod plugin.
1882
+ * Default resolver used by `@kubb/plugin-zod`. Decides the names and file
1883
+ * paths for every generated Zod schema. Schemas use camelCase with a
1884
+ * `Schema` suffix (`listPetsSchema`); their inferred types use PascalCase
1885
+ * with a `SchemaType` suffix (`PetSchemaType`), so the value and the type
1886
+ * never share an identifier even when the schema name is all-uppercase.
1025
1887
  *
1026
- * Provides default naming helpers using camelCase with a `Schema` suffix for schemas.
1888
+ * @example Resolve schema and type names
1889
+ * ```ts
1890
+ * import { resolverZod } from '@kubb/plugin-zod'
1027
1891
  *
1028
- * @example
1029
- * `resolverZod.default('list pets', 'function') // 'listPetsSchema'`
1892
+ * resolverZod.name('list pets') // 'listPetsSchema'
1893
+ * resolverZod.schema.typeName('pet') // 'PetSchemaType'
1894
+ * ```
1030
1895
  */
1031
- const resolverZod = defineResolver(() => {
1032
- return {
1033
- name: "default",
1034
- pluginName: "plugin-zod",
1035
- default(name, type) {
1036
- return camelCase(name, {
1037
- isFile: type === "file",
1038
- suffix: type ? "schema" : void 0
1039
- });
1040
- },
1041
- resolveSchemaName(name) {
1042
- return camelCase(name, { suffix: "schema" });
1043
- },
1044
- resolveSchemaTypeName(name) {
1045
- return pascalCase(name, { suffix: "schema" });
1046
- },
1047
- resolveTypeName(name) {
1048
- return pascalCase(name);
1049
- },
1050
- resolvePathName(name, type) {
1051
- return this.default(name, type);
1052
- },
1053
- resolveParamName(node, param) {
1054
- return this.resolveSchemaName(`${node.operationId} ${param.in} ${param.name}`);
1055
- },
1056
- resolveResponseStatusName(node, statusCode) {
1057
- return this.resolveSchemaName(`${node.operationId} Status ${statusCode}`);
1058
- },
1059
- resolveDataName(node) {
1060
- return this.resolveSchemaName(`${node.operationId} Data`);
1061
- },
1062
- resolveResponsesName(node) {
1063
- return this.resolveSchemaName(`${node.operationId} Responses`);
1896
+ const resolverZod = createResolver({
1897
+ pluginName: "plugin-zod",
1898
+ name(name) {
1899
+ return ensureValidVarName(camelCase(name, { suffix: "schema" }));
1900
+ },
1901
+ file: createCasedFile((part) => camelCase(part, { suffix: "schema" })),
1902
+ schema: {
1903
+ typeName(name) {
1904
+ return ensureValidVarName(pascalCase(name, { suffix: "schema type" }));
1064
1905
  },
1065
- resolveResponseName(node) {
1066
- return this.resolveSchemaName(`${node.operationId} Response`);
1906
+ type(name) {
1907
+ return ensureValidVarName(pascalCase(name, { suffix: "type" }));
1067
1908
  },
1068
- resolvePathParamsName(node, param) {
1069
- return this.resolveParamName(node, param);
1909
+ inputName(name) {
1910
+ return this.name(`${name} input`);
1070
1911
  },
1071
- resolveQueryParamsName(node, param) {
1072
- return this.resolveParamName(node, param);
1912
+ inputTypeName(name) {
1913
+ return this.schema.typeName(`${name} input`);
1914
+ }
1915
+ },
1916
+ param: createOperationParamResolver(),
1917
+ response: {
1918
+ ...createOperationResponseResolver(),
1919
+ error(node) {
1920
+ return this.name(`${node.operationId} Error`);
1073
1921
  },
1074
- resolveHeaderParamsName(node, param) {
1075
- return this.resolveParamName(node, param);
1922
+ options(node) {
1923
+ return this.schema.type(this.name(`${node.operationId} Options`));
1076
1924
  }
1077
- };
1925
+ }
1078
1926
  });
1079
1927
  //#endregion
1080
1928
  //#region src/plugin.ts
1081
1929
  /**
1082
- * Canonical plugin name for `@kubb/plugin-zod`, used in driver lookups and warnings.
1930
+ * Canonical plugin name for `@kubb/plugin-zod`. Used for driver lookups and
1931
+ * cross-plugin dependency references.
1083
1932
  */
1084
1933
  const pluginZodName = "plugin-zod";
1085
1934
  /**
1086
- * Generates Zod validation schemas from an OpenAPI specification.
1087
- * Walks schemas and operations, delegates to generators, and writes barrel files
1088
- * based on the configured `barrelType`.
1935
+ * Generates Zod v4 schemas from an OpenAPI spec. Use them to validate API
1936
+ * responses at runtime, build form schemas, or feed back into router libraries
1937
+ * that consume Zod (tRPC, Hono, Elysia). Pair with `@kubb/plugin-axios` or
1938
+ * `@kubb/plugin-fetch` and set the client's `validator: 'zod'` to validate every response
1939
+ * automatically.
1089
1940
  *
1090
- * @example Zod schema generator
1941
+ * @example
1091
1942
  * ```ts
1092
- * import pluginZod from '@kubb/plugin-zod'
1943
+ * import { defineConfig } from 'kubb/config'
1944
+ * import { pluginTs } from '@kubb/plugin-ts'
1945
+ * import { pluginZod } from '@kubb/plugin-zod'
1946
+ *
1093
1947
  * export default defineConfig({
1094
- * plugins: [pluginZod({ output: { path: 'zod' } })]
1948
+ * input: './petStore.yaml',
1949
+ * output: { path: './src/gen' },
1950
+ * plugins: [
1951
+ * pluginTs(),
1952
+ * pluginZod({
1953
+ * output: { path: './zod' },
1954
+ * inferred: true,
1955
+ * }),
1956
+ * ],
1095
1957
  * })
1096
1958
  * ```
1097
1959
  */
1098
1960
  const pluginZod = definePlugin((options) => {
1099
1961
  const { output = {
1100
1962
  path: "zod",
1101
- barrelType: "named"
1102
- }, 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;
1103
- const groupConfig = group ? {
1104
- ...group,
1105
- name: (ctx) => {
1106
- if (group.type === "path") return `${ctx.group.split("/")[1]}`;
1107
- return `${camelCase(ctx.group)}Controller`;
1108
- }
1109
- } : void 0;
1963
+ barrel: { type: "named" }
1964
+ }, group, exclude = [], include, override = [], mini = false, guidType = "uuid", regexType = "literal", importPath = mini ? "zod/mini" : "zod", coercion = false, inferred = false, printer, resolver: userResolver, macros: userMacros } = options;
1965
+ const groupConfig = createGroupConfig(group);
1110
1966
  return {
1111
1967
  name: pluginZodName,
1112
1968
  options,
@@ -1117,24 +1973,17 @@ const pluginZod = definePlugin((options) => {
1117
1973
  include,
1118
1974
  override,
1119
1975
  group: groupConfig,
1120
- typed,
1121
1976
  importPath,
1122
1977
  coercion,
1123
- operations,
1124
1978
  inferred,
1125
1979
  guidType,
1980
+ regexType,
1126
1981
  mini,
1127
- wrapOutput,
1128
- paramsCasing,
1129
1982
  printer
1130
1983
  });
1131
- ctx.setResolver(userResolver ? {
1132
- ...resolverZod,
1133
- ...userResolver
1134
- } : resolverZod);
1135
- if (userTransformer) ctx.setTransformer(userTransformer);
1984
+ ctx.setResolver(userResolver ? Resolver.merge(resolverZod, userResolver) : resolverZod);
1985
+ if (userMacros?.length) ctx.setMacros(userMacros);
1136
1986
  ctx.addGenerator(zodGenerator);
1137
- for (const gen of userGenerators) ctx.addGenerator(gen);
1138
1987
  } }
1139
1988
  };
1140
1989
  });