@kubb/plugin-zod 5.9.0-canary.20261111T122006 → 5.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,143 +1,407 @@
1
- import { Diagnostics, Resolver, addGenerator, ast, camelCase, containsCircularRef, createResolver, defineExtension, defineGenerator, pascalCase, setMacros, setResolver, syncSchemaRef } from "@kubb/kit";
1
+ import { Diagnostics, Resolver, addGenerator, ast, casing, createPrinter, createResolver, defineExtension, defineGenerator, setMacros, setResolver, text } from "@kubb/kit";
2
2
  import { Const, File, Function, Type, jsxRenderer } from "@kubb/renderer-jsx";
3
3
  import { Fragment, jsx, jsxs } from "@kubb/renderer-jsx/jsx-runtime";
4
- // ../../internals/shared/src/operation.ts
4
+ // ../../internals/utils/src/reserved.ts
5
5
  /**
6
- * Builds the `ResolverFileParams` every operation generator passes to
7
- * `resolver.file`: a file named `name`, tagged by the operation's first
8
- * tag (or `'default'`), at the operation's path. Centralizes the entry object
9
- * that was repeated at dozens of call sites across the client and query plugins.
6
+ * JavaScript and Java reserved words.
7
+ * @link https://github.com/jonschlinkert/reserved/blob/master/index.js
8
+ */
9
+ const reservedWords = /* @__PURE__ */ new Set([
10
+ "abstract",
11
+ "arguments",
12
+ "await",
13
+ "boolean",
14
+ "break",
15
+ "byte",
16
+ "case",
17
+ "catch",
18
+ "char",
19
+ "class",
20
+ "const",
21
+ "continue",
22
+ "debugger",
23
+ "default",
24
+ "delete",
25
+ "do",
26
+ "double",
27
+ "else",
28
+ "enum",
29
+ "eval",
30
+ "export",
31
+ "extends",
32
+ "false",
33
+ "final",
34
+ "finally",
35
+ "float",
36
+ "for",
37
+ "function",
38
+ "goto",
39
+ "if",
40
+ "implements",
41
+ "import",
42
+ "in",
43
+ "instanceof",
44
+ "int",
45
+ "interface",
46
+ "let",
47
+ "long",
48
+ "native",
49
+ "new",
50
+ "null",
51
+ "package",
52
+ "private",
53
+ "protected",
54
+ "public",
55
+ "return",
56
+ "short",
57
+ "static",
58
+ "super",
59
+ "switch",
60
+ "synchronized",
61
+ "this",
62
+ "throw",
63
+ "throws",
64
+ "transient",
65
+ "true",
66
+ "try",
67
+ "typeof",
68
+ "var",
69
+ "void",
70
+ "volatile",
71
+ "while",
72
+ "with",
73
+ "yield",
74
+ "Array",
75
+ "Date",
76
+ "hasOwnProperty",
77
+ "Infinity",
78
+ "isFinite",
79
+ "isNaN",
80
+ "isPrototypeOf",
81
+ "length",
82
+ "Math",
83
+ "name",
84
+ "NaN",
85
+ "Number",
86
+ "Object",
87
+ "prototype",
88
+ "String",
89
+ "toString",
90
+ "undefined",
91
+ "valueOf"
92
+ ]);
93
+ /**
94
+ * Returns `true` when `name` is a syntactically valid JavaScript variable name.
10
95
  *
11
96
  * @example
12
97
  * ```ts
13
- * resolver.file(operationFileEntry(node, node.operationId), { root, output, group })
98
+ * isValidVarName('status') // true
99
+ * isValidVarName('class') // false (reserved word)
100
+ * isValidVarName('42foo') // false (starts with digit)
14
101
  * ```
15
102
  */
16
- function operationFileEntry(node, name, extname = ".ts") {
17
- return {
18
- name,
19
- extname,
20
- tag: node.tags[0] ?? "default",
21
- path: node.path
22
- };
103
+ function isValidVarName(name) {
104
+ if (!name || reservedWords.has(name)) return false;
105
+ return isIdentifier(name);
23
106
  }
24
107
  /**
25
- * Resolves the file a schema-emitting plugin (`plugin-ts`, `plugin-zod`, `plugin-faker`) writes
26
- * `node` to, cached in `ctx.memo` under the resolver's plugin name and the
27
- * operation. The plugin itself and every plugin that imports from it call this, so they all get
28
- * the same path.
108
+ * Returns `name` when it's a syntactically valid JavaScript variable name,
109
+ * otherwise prefixes it with `_` so the result is a valid identifier.
29
110
  *
30
- * When that path matches the file of a component schema in `ctx.driver.inputNode`, ignoring case, the file name
31
- * gets an `Operation` suffix: operation `point` goes to `PointOperation.ts` next to schema `Point.ts`.
32
- * Otherwise the two merge into one file, or overwrite each other on a case-insensitive file system.
33
- * Only the file name changes. The declarations keep their names.
111
+ * Useful for sanitizing OpenAPI schema names or operation IDs that start with
112
+ * a digit (e.g. `409`, `504AccountCancel`) before using them as exported
113
+ * variable, type, or function names.
34
114
  *
35
- * @example Resolve `plugin-ts`'s file for the current operation
115
+ * @example
36
116
  * ```ts
37
- * const fileTs = resolveOperationFile({ ctx, node, resolver: tsResolver, root, output })
117
+ * ensureValidVarName('409') // '_409'
118
+ * ensureValidVarName('504AccountCancel') // '_504AccountCancel'
119
+ * ensureValidVarName('Pet') // 'Pet'
120
+ * ensureValidVarName('class') // '_class'
38
121
  * ```
39
122
  */
40
- function resolveOperationFile(options) {
41
- const { ctx, node, resolver, root, output } = options;
42
- const { memo } = ctx;
43
- const location = {
44
- root,
45
- output,
46
- group: options.group ?? void 0
47
- };
48
- return memo(`${resolver.pluginName}:operationFile:${node.operationId}`, () => {
49
- const schemaPaths = memo(`${resolver.pluginName}:schemaFilePaths`, () => new Set((ctx.driver.inputNode?.schemas ?? []).flatMap((schema) => schema.name ? [resolver.file({
50
- name: schema.name,
51
- extname: ".ts",
52
- ...location
53
- }).path.toLowerCase()] : [])));
54
- let file = resolver.file({
55
- ...operationFileEntry(node, node.operationId),
56
- ...location
57
- });
58
- for (let name = `${node.operationId}Operation`; schemaPaths.has(file.path.toLowerCase()); name += "Operation") {
59
- const next = resolver.file({
60
- ...operationFileEntry(node, name),
61
- ...location
62
- });
63
- if (next.path === file.path) break;
64
- file = next;
65
- }
66
- return file;
67
- });
68
- }
69
- function baseContentType(contentType) {
70
- return contentType.split(";")[0].trim().toLowerCase();
123
+ function ensureValidVarName(name) {
124
+ if (!name || isValidVarName(name)) return name;
125
+ return `_${name}`;
71
126
  }
72
127
  /**
73
- * Whether a content entry is a sequence of JSON values the adapter marks as a stream, such as `application/jsonl`,
74
- * `application/x-ndjson`, `application/json-seq`, or any media type with an OpenAPI 3.2 `itemSchema`.
128
+ * Returns `true` when `name` is syntactically a valid identifier, ignoring reserved words.
129
+ *
130
+ * Reserved words and globals (`class`, `name`, `Date`, …) are valid as bare object-literal keys
131
+ * even though they are not valid variable names, so use this (not {@link isValidVarName}) when
132
+ * deciding whether an object key needs quoting.
133
+ *
134
+ * @example
135
+ * ```ts
136
+ * isIdentifier('name') // true
137
+ * isIdentifier('x-total')// false
138
+ * ```
75
139
  */
76
- function isJsonLines(entry) {
77
- const baseType = baseContentType(entry.contentType);
78
- return Boolean(entry.stream) && baseType !== "text/event-stream" && !baseType.startsWith("multipart/");
79
- }
80
- function isJsonContentType(baseType) {
81
- return baseType === "application/json" || baseType.endsWith("+json") || baseType === "text/json";
140
+ function isIdentifier(name) {
141
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
82
142
  }
143
+ // ../../internals/utils/src/codegen.ts
83
144
  /**
84
- * Parser for one response content entry.
145
+ * Assembles a multi-line object literal from already-rendered `entries`, indenting each entry one
146
+ * level and closing the brace at column zero. Entries that are themselves multi-line objects indent
147
+ * cumulatively. Each entry ends with a trailing comma to match the formatter's multi-line style.
85
148
  *
86
- * JSON stays unset so the runtime detects it. `text/event-stream` and JSON Lines are streams. A binary schema
87
- * (`format: binary` / `contentMediaType`) is a blob even when the media type is not a well-known
88
- * binary type. Other `text/*` is text, and `application/octet-stream`, `application/pdf`,
89
- * `image/*`, `audio/*`, and `video/*` are blobs.
149
+ * @example
150
+ * ```ts
151
+ * buildObject(['id: z.number()', 'name: z.string()'])
152
+ * // '{\n id: z.number(),\n name: z.string(),\n}'
153
+ * ```
90
154
  */
91
- function responseTypeForContent(entry) {
92
- const baseType = baseContentType(entry.contentType);
93
- if (!baseType) return void 0;
94
- if (baseType === "text/event-stream" || isJsonLines(entry)) return "stream";
95
- if (isJsonContentType(baseType)) return void 0;
96
- if (entry.schema?.type === "blob") return "blob";
97
- if (baseType.startsWith("text/")) return "text";
98
- if (isBinaryContentType(baseType)) return "blob";
99
- }
100
- function isBinaryContentType(baseType) {
101
- return baseType === "application/octet-stream" || baseType === "application/pdf" || /^(image|audio|video)\//.test(baseType);
155
+ function buildObject(entries) {
156
+ if (entries.length === 0) return "{}";
157
+ return `{\n${entries.map((entry) => `${text.indent({ text: entry })},`).join("\n")}\n}`;
102
158
  }
103
159
  /**
104
- * Maps a content type to the PascalCase suffix used to name per-content-type variants
105
- * (e.g. `application/json` → `Json`, `application/xml` → `Xml`, `multipart/form-data` → `FormData`).
160
+ * Assembles a bracketed list (array by default) from already-rendered `items`. Keeps everything on
161
+ * one line when no item spans multiple lines, and otherwise puts each item on its own line, indented
162
+ * one level with a trailing comma and the closing bracket at column zero. Used for member lists such
163
+ * as `z.union([…])` and `z.array([…])`.
164
+ *
165
+ * @example
166
+ * ```ts
167
+ * buildList(['z.string()', 'z.number()'])
168
+ * // '[z.string(), z.number()]'
169
+ * ```
106
170
  */
107
- function getContentTypeSuffix(contentType) {
108
- const baseType = contentType.split(";")[0].trim();
109
- if (baseType === "application/json") return "Json";
110
- if (baseType === "multipart/form-data") return "FormData";
111
- if (baseType === "application/x-www-form-urlencoded") return "FormUrlEncoded";
112
- const parts = (baseType.split("/").pop() ?? baseType).split(/[^a-zA-Z0-9]+/).filter(Boolean);
113
- if (parts.length === 0) return "Unknown";
114
- return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
171
+ function buildList(items, brackets = ["[", "]"]) {
172
+ const [open, close] = brackets;
173
+ if (items.length === 0) return `${open}${close}`;
174
+ if (!items.some((item) => item.includes("\n"))) return `${open}${items.join(", ")}${close}`;
175
+ return `${open}\n${items.map((item) => `${text.indent({ text: item })},`).join("\n")}\n${close}`;
115
176
  }
116
177
  /**
117
- * Appends a content-type suffix to a base name, keeping a trailing `Data` segment last
118
- * (e.g. `AddPetData` + `Json` → `AddPetJsonData`, `AddPetStatus200` + `Xml` → `AddPetStatus200Xml`).
178
+ * Emits a lazy getter for a circular-ref property position, `get name() { return body }`. The key
179
+ * is quoted only when it is not a valid identifier. Used by the string printers to defer evaluation
180
+ * of a recursive schema until first access.
181
+ *
182
+ * @example
183
+ * ```ts
184
+ * lazyGetter({ name: 'parent', body: 'z.lazy(() => Pet)' })
185
+ * // "get parent() { return z.lazy(() => Pet) }"
186
+ * ```
119
187
  */
120
- function getPerContentTypeName(baseName, suffix) {
121
- if (baseName.endsWith("Data")) return suffix.endsWith("Data") ? baseName.slice(0, -4) + suffix : `${baseName.slice(0, -4)}${suffix}Data`;
122
- return baseName + suffix;
188
+ function lazyGetter({ name, body }) {
189
+ return `get ${objectKey(name)}() { return ${body} }`;
123
190
  }
124
191
  /**
125
- * Returns the entry with the schema of the value the client returns: `string` for a body read as text, such as `Pet` on
126
- * `application/xml`, and `blob` for `image/*`, `audio/*`, `video/*`, PDF, and octet-stream. A binary schema elsewhere is a `Blob`
127
- * only through `parseAs: 'blob'`, which needs every entry in `entries` binary. JSON, vendor (`application/vnd.*`), stream,
128
- * schemaless, and wildcard entries keep their schema. `entries` is the response's full list of content types.
192
+ * An object key, quoted only when it is not identifier-shaped. Reserved words such as `class` stay bare.
193
+ *
194
+ * @example
195
+ * ```ts
196
+ * objectKey('x-total') // "'x-total'"
197
+ * ```
129
198
  */
130
- function toResponseContent(entry, entries = [entry]) {
131
- const { contentType, schema } = entry;
132
- if (!schema || entry.stream) return entry;
133
- const baseType = baseContentType(contentType);
134
- const resolved = ast.resolveRef(schema);
135
- if (isBinaryContentType(baseType)) return resolved.type === "blob" ? entry : {
136
- ...entry,
137
- schema: ast.factory.createSchema({ type: "blob" }),
138
- keysToOmit: null
139
- };
140
- const isPlainMediaType = Boolean(baseType) && !isJsonContentType(baseType) && !baseType.includes("*");
199
+ function objectKey(name) {
200
+ return isIdentifier(name) ? name : text.quote(name);
201
+ }
202
+ // ../../internals/utils/src/fs.ts
203
+ /**
204
+ * Builds a nested file path from a dotted name. Splits on dots that precede a letter
205
+ * (so version numbers embedded in operationIds like `v2025.0` stay intact), camelCases
206
+ * every earlier segment, applies `caseLast` to the final segment, and joins with `/`.
207
+ *
208
+ * Empty segments are dropped before joining. They arise when the name starts with a dot
209
+ * followed by a letter (e.g. `..Schema` splits into `['..', 'Schema']` and `'..'` cases to
210
+ * an empty string). Without this a leading `/` would form, which `path.resolve` reads as an
211
+ * absolute path, letting generated files escape the configured output directory.
212
+ *
213
+ * @example Nested path from a dotted name
214
+ * `toFilePath('pet.petId') // 'pet/petId'`
215
+ *
216
+ * @example PascalCase the final segment
217
+ * `toFilePath('pet.Pet', pascalCase) // 'pet/Pet'`
218
+ *
219
+ * @example Suffix applied to the final segment only
220
+ * `toFilePath('tag.tag', (part) => camelCase(part, { suffix: 'schema' })) // 'tag/tagSchema'`
221
+ */
222
+ function toFilePath(name, caseLast = casing.camel) {
223
+ const parts = name.split(/\.(?=[a-zA-Z])/);
224
+ return parts.map((part, i) => i === parts.length - 1 ? caseLast(part) : casing.camel(part)).filter(Boolean).join("/");
225
+ }
226
+ // ../../internals/utils/src/strings.ts
227
+ const NEEDS_UNICODE = /(?<!\\)(?:\\\\)*\\[pPu]\{|[\u{10000}-\u{10FFFF}]/u;
228
+ const KEPT_ESCAPE = /[A-Za-z0-9^$\\.*+?()[\]{}|/]/;
229
+ /**
230
+ * Drops the backslash from escapes that the `u` flag rejects: an escaped punctuation character that is
231
+ * not a syntax character, and `\-` outside a class. Both match the same character without the backslash.
232
+ */
233
+ function dropIdentityEscapes(pattern) {
234
+ let inClass = false;
235
+ return pattern.replace(/\\(.)|[[\]]/gs, (match, escaped) => {
236
+ if (escaped === void 0) {
237
+ inClass = match === "[";
238
+ return match;
239
+ }
240
+ return KEPT_ESCAPE.test(escaped) || escaped === "-" && inClass ? match : escaped;
241
+ });
242
+ }
243
+ /**
244
+ * Builds the `RegExp` for a pattern, adding the `u` flag when it uses `\p{...}`, `\P{...}` or `\u{...}`, or
245
+ * holds a character outside the BMP. A pattern that is not valid under `u` keeps the flags it came with.
246
+ */
247
+ function toRegExp(pattern, flags) {
248
+ if (flags?.includes("u") || !NEEDS_UNICODE.test(pattern)) return new RegExp(pattern, flags);
249
+ try {
250
+ return new RegExp(dropIdentityEscapes(pattern), `${flags ?? ""}u`);
251
+ } catch {
252
+ return new RegExp(pattern, flags);
253
+ }
254
+ }
255
+ /**
256
+ * Converts a pattern string into a `new RegExp(...)` constructor call or a regex literal string.
257
+ * Inline flags expressed as a `^(?im)` prefix are extracted and applied to the resulting expression.
258
+ * Pass `null` as the second argument to emit a `/pattern/flags` literal instead.
259
+ *
260
+ * @example
261
+ * ```ts
262
+ * toRegExpString('^(?im)foo') // 'new RegExp("^foo", "im")'
263
+ * toRegExpString('^(?im)foo', null) // '/^foo/im'
264
+ * ```
265
+ */
266
+ function toRegExpString(pattern, func = "RegExp") {
267
+ const raw = text.unquote(pattern);
268
+ const match = raw.match(/^\^(\(\?([igmsuy]+)\))/i);
269
+ const replacementTarget = match?.[1] ?? "";
270
+ const matchedFlags = match?.[2];
271
+ const { source, flags } = toRegExp(raw.replace(/^\\?\//, "").replace(/\\?\/$/, "").replace(replacementTarget, ""), matchedFlags);
272
+ if (func === null) return `/${source}/${flags}`;
273
+ return `new ${func}(${JSON.stringify(source)}${flags ? `, ${JSON.stringify(flags)}` : ""})`;
274
+ }
275
+ // ../../internals/shared/src/operation.ts
276
+ /**
277
+ * Builds the `ResolverFileParams` every operation generator passes to
278
+ * `resolver.file`: a file named `name`, tagged by the operation's first
279
+ * tag (or `'default'`), at the operation's path. Centralizes the entry object
280
+ * that was repeated at dozens of call sites across the client and query plugins.
281
+ *
282
+ * @example
283
+ * ```ts
284
+ * resolver.file(operationFileEntry(node, node.operationId), { root, output, group })
285
+ * ```
286
+ */
287
+ function operationFileEntry(node, name, extname = ".ts") {
288
+ return {
289
+ name,
290
+ extname,
291
+ tag: node.tags[0] ?? "default",
292
+ path: node.path
293
+ };
294
+ }
295
+ /**
296
+ * Resolves the file a schema-emitting plugin (`plugin-ts`, `plugin-zod`, `plugin-faker`) writes
297
+ * `node` to, cached in `ctx.memo` under the resolver's plugin name and the
298
+ * operation. The plugin itself and every plugin that imports from it call this, so they all get
299
+ * the same path.
300
+ *
301
+ * When that path matches the file of a component schema in `ctx.driver.inputNode`, ignoring case, the file name
302
+ * gets an `Operation` suffix: operation `point` goes to `PointOperation.ts` next to schema `Point.ts`.
303
+ * Otherwise the two merge into one file, or overwrite each other on a case-insensitive file system.
304
+ * Only the file name changes. The declarations keep their names.
305
+ *
306
+ * @example Resolve `plugin-ts`'s file for the current operation
307
+ * ```ts
308
+ * const fileTs = resolveOperationFile({ ctx, node, resolver: tsResolver, root, output })
309
+ * ```
310
+ */
311
+ function resolveOperationFile(options) {
312
+ const { ctx, node, resolver, root, output } = options;
313
+ const { memo } = ctx;
314
+ const location = {
315
+ root,
316
+ output,
317
+ group: options.group ?? void 0
318
+ };
319
+ return memo(`${resolver.pluginName}:operationFile:${node.operationId}`, () => {
320
+ const schemaPaths = memo(`${resolver.pluginName}:schemaFilePaths`, () => new Set((ctx.driver.inputNode?.schemas ?? []).flatMap((schema) => schema.name ? [resolver.file({
321
+ name: schema.name,
322
+ extname: ".ts",
323
+ ...location
324
+ }).path.toLowerCase()] : [])));
325
+ let file = resolver.file({
326
+ ...operationFileEntry(node, node.operationId),
327
+ ...location
328
+ });
329
+ for (let name = `${node.operationId}Operation`; schemaPaths.has(file.path.toLowerCase()); name += "Operation") {
330
+ const next = resolver.file({
331
+ ...operationFileEntry(node, name),
332
+ ...location
333
+ });
334
+ if (next.path === file.path) break;
335
+ file = next;
336
+ }
337
+ return file;
338
+ });
339
+ }
340
+ /**
341
+ * How a content entry is framed as a stream: `'events'` for Server-Sent Events, `'lines'` for JSON Lines.
342
+ */
343
+ function getStreamFraming(entry) {
344
+ if (ast.mediaType.getBase(entry) === "text/event-stream") return "events";
345
+ return entry.stream && ast.mediaType.isStream(entry) ? "lines" : void 0;
346
+ }
347
+ /**
348
+ * Parser for one response content entry.
349
+ *
350
+ * JSON stays unset so the runtime detects it. `text/event-stream` and JSON Lines are streams. A binary schema
351
+ * (`format: binary` / `contentMediaType`) is a blob even when the media type is not a well-known
352
+ * binary type. Other `text/*` is text, and `application/octet-stream`, `application/pdf`,
353
+ * `image/*`, `audio/*`, and `video/*` are blobs.
354
+ */
355
+ function responseTypeForContent(entry) {
356
+ const baseType = ast.mediaType.getBase(entry);
357
+ if (!baseType) return void 0;
358
+ if (getStreamFraming(entry)) return "stream";
359
+ if (ast.mediaType.isJson(baseType)) return void 0;
360
+ if (entry.schema?.type === "blob") return "blob";
361
+ if (baseType.startsWith("text/")) return "text";
362
+ if (isBinaryContentType(baseType)) return "blob";
363
+ }
364
+ function isBinaryContentType(baseType) {
365
+ return baseType === "application/octet-stream" || baseType === "application/pdf" || /^(image|audio|video)\//.test(baseType);
366
+ }
367
+ /**
368
+ * Maps a content type to the PascalCase suffix used to name per-content-type variants
369
+ * (e.g. `application/json` → `Json`, `application/xml` → `Xml`, `multipart/form-data` → `FormData`).
370
+ */
371
+ function getContentTypeSuffix(contentType) {
372
+ const baseType = contentType.split(";")[0].trim();
373
+ if (baseType === "application/json") return "Json";
374
+ if (baseType === "multipart/form-data") return "FormData";
375
+ if (baseType === "application/x-www-form-urlencoded") return "FormUrlEncoded";
376
+ const parts = (baseType.split("/").pop() ?? baseType).split(/[^a-zA-Z0-9]+/).filter(Boolean);
377
+ if (parts.length === 0) return "Unknown";
378
+ return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
379
+ }
380
+ /**
381
+ * Appends a content-type suffix to a base name, keeping a trailing `Data` segment last
382
+ * (e.g. `AddPetData` + `Json` → `AddPetJsonData`, `AddPetStatus200` + `Xml` → `AddPetStatus200Xml`).
383
+ */
384
+ function getPerContentTypeName(baseName, suffix) {
385
+ if (baseName.endsWith("Data")) return suffix.endsWith("Data") ? baseName.slice(0, -4) + suffix : `${baseName.slice(0, -4)}${suffix}Data`;
386
+ return baseName + suffix;
387
+ }
388
+ /**
389
+ * Returns the entry with the schema of the value the client returns: `string` for a body read as text, such as `Pet` on
390
+ * `application/xml`, and `blob` for `image/*`, `audio/*`, `video/*`, PDF, and octet-stream. A binary schema elsewhere is a `Blob`
391
+ * only through `parseAs: 'blob'`, which needs every entry in `entries` binary. JSON, vendor (`application/vnd.*`), stream,
392
+ * schemaless, and wildcard entries keep their schema. `entries` is the response's full list of content types.
393
+ */
394
+ function toResponseContent(entry, entries = [entry]) {
395
+ const { contentType, schema } = entry;
396
+ if (!schema || entry.stream) return entry;
397
+ const baseType = ast.mediaType.getBase(contentType);
398
+ const resolved = ast.ref.getSchema(schema);
399
+ if (isBinaryContentType(baseType)) return resolved.type === "blob" ? entry : {
400
+ ...entry,
401
+ schema: ast.factory.createSchema({ type: "blob" }),
402
+ keysToOmit: null
403
+ };
404
+ const isPlainMediaType = Boolean(baseType) && !ast.mediaType.isJson(baseType) && !baseType.includes("*");
141
405
  if (resolved.type === "blob") return isPlainMediaType && !entries.every((item) => item.schema?.type === "blob") ? {
142
406
  ...entry,
143
407
  schema: ast.factory.createSchema({ type: "string" }),
@@ -180,50 +444,6 @@ function resolveContentTypeVariants(entries, baseName) {
180
444
  };
181
445
  });
182
446
  }
183
- const operationParameterGroupsByNode = /* @__PURE__ */ new WeakMap();
184
- /**
185
- * An OpenAPI 3.2 `querystring` parameter is the whole query string, so a form-encoded object becomes one query parameter
186
- * per property, typed and serialized like any other.
187
- * Another content type, such as JSON, stays one parameter named after it, and the runtime sends its value as the whole query string.
188
- */
189
- function spreadQuerystring(param) {
190
- const schema = param.schema.type === "ref" ? param.schema.schema : param.schema;
191
- const formEncoded = !param.contentType || param.contentType.split(";")[0].trim().toLowerCase() === "application/x-www-form-urlencoded";
192
- if (schema?.type !== "object" || !formEncoded) return [param];
193
- return schema.properties.map((property) => ast.factory.createParameter({
194
- name: property.name,
195
- in: "query",
196
- schema: property.schema,
197
- required: param.required && property.required
198
- }));
199
- }
200
- /**
201
- * Groups an operation's parameters by location (`path`/`query`/`header`/`cookie`), deduping each
202
- * group by name. Every plugin generator visiting the same `OperationNode` shares one AST instance
203
- * (see `KubbDriver`), so the result is cached per node to avoid re-filtering and re-deduping the
204
- * same parameters once per plugin.
205
- */
206
- function getOperationParameters(node) {
207
- const cached = operationParameterGroupsByNode.get(node);
208
- if (cached) return cached;
209
- const dedupe = (params) => {
210
- const seen = /* @__PURE__ */ new Set();
211
- return params.filter((param) => {
212
- if (seen.has(param.name)) return false;
213
- seen.add(param.name);
214
- return true;
215
- });
216
- };
217
- const group = (location) => dedupe(node.parameters.filter((param) => param.in === location));
218
- const groups = {
219
- path: group("path"),
220
- query: dedupe([...group("query"), ...group("querystring").flatMap(spreadQuerystring)]),
221
- header: group("header"),
222
- cookie: group("cookie")
223
- };
224
- operationParameterGroupsByNode.set(node, groups);
225
- return groups;
226
- }
227
447
  /**
228
448
  * Whether the operation declares more than one request media type with a schema, so a caller must pick one on `contentType`.
229
449
  */
@@ -239,8 +459,8 @@ function hasRequestContentTypeChoice(node) {
239
459
  * than a record.
240
460
  */
241
461
  function buildOptionsSchema(node, resolver, { pairContentTypes = false } = {}) {
242
- const { path, query, header, cookie } = getOperationParameters(node);
243
- const hasBody = Boolean(node.requestBody?.content?.[0]?.schema);
462
+ const { path, query, header, cookie } = ast.http.groupParameters(node);
463
+ const hasBody = Boolean(ast.http.requestBody.getSchema(node));
244
464
  const createNever = () => ast.factory.createSchema({
245
465
  type: "never",
246
466
  primitive: void 0,
@@ -313,438 +533,101 @@ function buildOptionsSchema(node, resolver, { pairContentTypes = false } = {}) {
313
533
  required: index > 0,
314
534
  schema: ast.factory.createSchema({
315
535
  type: "union",
316
- members: [contentType, ast.factory.createSchema({
317
- type: "object",
318
- primitive: "object",
319
- properties: [ast.factory.createProperty({
320
- name: "request",
321
- required: true,
322
- schema: contentType
323
- })]
324
- })]
325
- })
326
- })]);
327
- })
328
- });
329
- }
330
- /**
331
- * The schema a status occupies in the `<Name>Responses` record. A status that documents several
332
- * content types becomes a `{ contentType; data }` union so the runtime can surface the negotiated type
333
- * on `result.parsed`, while the standalone `<Name>StatusNNN` alias stays the plain body union that the
334
- * query hooks and `result.data` use.
335
- */
336
- function buildResponseRecordEntry(node, res, resolver) {
337
- const statusName = resolver.response.status(node, res.statusCode);
338
- const variants = (res.content ?? []).filter((entry) => entry.schema);
339
- if (variants.length <= 1) return ast.factory.createSchema({
340
- type: "ref",
341
- name: statusName
342
- });
343
- return ast.factory.createSchema({
344
- type: "union",
345
- members: resolveContentTypeVariants(variants, statusName).map((variant) => ast.factory.createSchema({
346
- type: "object",
347
- primitive: "object",
348
- properties: [ast.factory.createProperty({
349
- name: "contentType",
350
- required: true,
351
- schema: ast.factory.createSchema({
352
- type: "enum",
353
- enumValues: [baseContentType(variant.contentType)]
354
- })
355
- }), ast.factory.createProperty({
356
- name: "data",
357
- required: true,
358
- schema: ast.factory.createSchema({
359
- type: "ref",
360
- name: variant.name
361
- })
362
- })]
363
- }))
364
- });
365
- }
366
- /**
367
- * Builds the per-status `<Name>Responses` record for an operation, referencing the already-resolved
368
- * `<Name>StatusNNN` names. Shared by `@kubb/plugin-ts`'s `Responses` type and `@kubb/plugin-zod`'s
369
- * inferred responses schema, so both emit the same shape from the same inputs.
370
- *
371
- * Always emits the keyed record, even when an operation declares no responses. An operation with no
372
- * responses renders as an empty object, which keeps every consumer's import (for example the axios
373
- * SDK's `RequestResult<XResponses>`) resolvable instead of pointing at a missing export.
374
- */
375
- function buildResponses(node, resolver) {
376
- return ast.factory.createSchema({
377
- type: "object",
378
- primitive: "object",
379
- properties: node.responses.map((res) => ast.factory.createProperty({
380
- name: String(res.statusCode),
381
- required: true,
382
- schema: buildResponseRecordEntry(node, res, resolver)
383
- }))
384
- });
385
- }
386
- function getStatusCodeNumber(statusCode) {
387
- const code = Number(statusCode);
388
- return Number.isNaN(code) ? null : code;
389
- }
390
- function isSuccessStatusCode(statusCode) {
391
- const code = getStatusCodeNumber(statusCode);
392
- return code !== null && code >= 200 && code < 300;
393
- }
394
- function getSuccessResponses(responses) {
395
- return responses.filter((response) => isSuccessStatusCode(response.statusCode));
396
- }
397
- // ../../internals/shared/src/adapter.ts
398
- /**
399
- * Narrows the generic `Adapter` from a generator context to the OpenAPI adapter,
400
- * so OAS-only options (`dateType`, `enums`) and the parsed `document` are typed.
401
- *
402
- * Throws when a non-OAS adapter is configured, turning a silently wrong cast into a
403
- * clear, actionable error at the point of use.
404
- *
405
- * @example
406
- * ```ts
407
- * const { dateType } = getOasAdapter(ctx.adapter).options
408
- * ```
409
- */
410
- function getOasAdapter(adapter) {
411
- if (adapter.name !== "oas") throw new Diagnostics.Error({
412
- code: Diagnostics.code.adapterRequired,
413
- severity: "error",
414
- message: `Expected the OpenAPI adapter (adapterOas), but received "${adapter.name}".`,
415
- help: "Set `adapter: adapterOas()` in kubb.config.ts.",
416
- location: { kind: "config" }
417
- });
418
- return adapter;
419
- }
420
- // ../../internals/utils/src/reserved.ts
421
- /**
422
- * JavaScript and Java reserved words.
423
- * @link https://github.com/jonschlinkert/reserved/blob/master/index.js
424
- */
425
- const reservedWords = /* @__PURE__ */ new Set([
426
- "abstract",
427
- "arguments",
428
- "await",
429
- "boolean",
430
- "break",
431
- "byte",
432
- "case",
433
- "catch",
434
- "char",
435
- "class",
436
- "const",
437
- "continue",
438
- "debugger",
439
- "default",
440
- "delete",
441
- "do",
442
- "double",
443
- "else",
444
- "enum",
445
- "eval",
446
- "export",
447
- "extends",
448
- "false",
449
- "final",
450
- "finally",
451
- "float",
452
- "for",
453
- "function",
454
- "goto",
455
- "if",
456
- "implements",
457
- "import",
458
- "in",
459
- "instanceof",
460
- "int",
461
- "interface",
462
- "let",
463
- "long",
464
- "native",
465
- "new",
466
- "null",
467
- "package",
468
- "private",
469
- "protected",
470
- "public",
471
- "return",
472
- "short",
473
- "static",
474
- "super",
475
- "switch",
476
- "synchronized",
477
- "this",
478
- "throw",
479
- "throws",
480
- "transient",
481
- "true",
482
- "try",
483
- "typeof",
484
- "var",
485
- "void",
486
- "volatile",
487
- "while",
488
- "with",
489
- "yield",
490
- "Array",
491
- "Date",
492
- "hasOwnProperty",
493
- "Infinity",
494
- "isFinite",
495
- "isNaN",
496
- "isPrototypeOf",
497
- "length",
498
- "Math",
499
- "name",
500
- "NaN",
501
- "Number",
502
- "Object",
503
- "prototype",
504
- "String",
505
- "toString",
506
- "undefined",
507
- "valueOf"
508
- ]);
509
- /**
510
- * Returns `true` when `name` is a syntactically valid JavaScript variable name.
511
- *
512
- * @example
513
- * ```ts
514
- * isValidVarName('status') // true
515
- * isValidVarName('class') // false (reserved word)
516
- * isValidVarName('42foo') // false (starts with digit)
517
- * ```
518
- */
519
- function isValidVarName(name) {
520
- if (!name || reservedWords.has(name)) return false;
521
- return isIdentifier(name);
522
- }
523
- /**
524
- * Returns `name` when it's a syntactically valid JavaScript variable name,
525
- * otherwise prefixes it with `_` so the result is a valid identifier.
526
- *
527
- * Useful for sanitizing OpenAPI schema names or operation IDs that start with
528
- * a digit (e.g. `409`, `504AccountCancel`) before using them as exported
529
- * variable, type, or function names.
530
- *
531
- * @example
532
- * ```ts
533
- * ensureValidVarName('409') // '_409'
534
- * ensureValidVarName('504AccountCancel') // '_504AccountCancel'
535
- * ensureValidVarName('Pet') // 'Pet'
536
- * ensureValidVarName('class') // '_class'
537
- * ```
538
- */
539
- function ensureValidVarName(name) {
540
- if (!name || isValidVarName(name)) return name;
541
- return `_${name}`;
542
- }
543
- /**
544
- * Returns `true` when `name` is syntactically a valid identifier, ignoring reserved words.
545
- *
546
- * Reserved words and globals (`class`, `name`, `Date`, …) are valid as bare object-literal keys
547
- * even though they are not valid variable names, so use this (not {@link isValidVarName}) when
548
- * deciding whether an object key needs quoting.
549
- *
550
- * @example
551
- * ```ts
552
- * isIdentifier('name') // true
553
- * isIdentifier('x-total')// false
554
- * ```
555
- */
556
- function isIdentifier(name) {
557
- return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
558
- }
559
- // ../../internals/utils/src/strings.ts
560
- /**
561
- * Wraps a value in single quotes for emitting a single-quoted JavaScript string literal, escaping
562
- * any backslash or single quote in the content.
563
- *
564
- * @example
565
- * ```ts
566
- * singleQuote('foo') // "'foo'"
567
- * singleQuote("o'clock") // "'o\\'clock'"
568
- * ```
569
- */
570
- function singleQuote(value) {
571
- if (value === void 0 || value === null) return "''";
572
- return `'${String(value).replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
573
- }
574
- /**
575
- * Strips a single matching pair of `"..."`, `'...'`, or `` `...` `` from both ends of `text`.
576
- * Returns the string unchanged when no balanced quote pair is found.
577
- *
578
- * @example
579
- * ```ts
580
- * trimQuotes('"hello"') // 'hello'
581
- * trimQuotes('hello') // 'hello'
582
- * ```
583
- */
584
- function trimQuotes(text) {
585
- if (text.length >= 2) {
586
- const first = text[0];
587
- const last = text[text.length - 1];
588
- if (first === "\"" && last === "\"" || first === "'" && last === "'" || first === "`" && last === "`") return text.slice(1, -1);
589
- }
590
- return text;
591
- }
592
- /**
593
- * Serializes a primitive to a single-quoted string literal, stripping any surrounding quotes first.
594
- *
595
- * Escaping runs through `JSON.stringify`, then the result switches to single quotes so the generated
596
- * code matches the repo style without a formatter.
597
- *
598
- * @example
599
- * ```ts
600
- * stringify('hello') // "'hello'"
601
- * stringify('"hello"') // "'hello'"
602
- * ```
603
- */
604
- function stringify(value) {
605
- if (value === void 0 || value === null) return "''";
606
- return `'${JSON.stringify(trimQuotes(value.toString())).slice(1, -1).replace(/\\"/g, "\"").replace(/'/g, "\\'")}'`;
607
- }
608
- const NEEDS_UNICODE = /(?<!\\)(?:\\\\)*\\[pPu]\{|[\u{10000}-\u{10FFFF}]/u;
609
- const KEPT_ESCAPE = /[A-Za-z0-9^$\\.*+?()[\]{}|/]/;
610
- /**
611
- * Drops the backslash from escapes that the `u` flag rejects: an escaped punctuation character that is
612
- * not a syntax character, and `\-` outside a class. Both match the same character without the backslash.
613
- */
614
- function dropIdentityEscapes(pattern) {
615
- let inClass = false;
616
- return pattern.replace(/\\(.)|[[\]]/gs, (match, escaped) => {
617
- if (escaped === void 0) {
618
- inClass = match === "[";
619
- return match;
620
- }
621
- return KEPT_ESCAPE.test(escaped) || escaped === "-" && inClass ? match : escaped;
536
+ members: [contentType, ast.factory.createSchema({
537
+ type: "object",
538
+ primitive: "object",
539
+ properties: [ast.factory.createProperty({
540
+ name: "request",
541
+ required: true,
542
+ schema: contentType
543
+ })]
544
+ })]
545
+ })
546
+ })]);
547
+ })
622
548
  });
623
549
  }
624
550
  /**
625
- * Builds the `RegExp` for a pattern, adding the `u` flag when it uses `\p{...}`, `\P{...}` or `\u{...}`, or
626
- * holds a character outside the BMP. A pattern that is not valid under `u` keeps the flags it came with.
627
- */
628
- function toRegExp(pattern, flags) {
629
- if (flags?.includes("u") || !NEEDS_UNICODE.test(pattern)) return new RegExp(pattern, flags);
630
- try {
631
- return new RegExp(dropIdentityEscapes(pattern), `${flags ?? ""}u`);
632
- } catch {
633
- return new RegExp(pattern, flags);
634
- }
635
- }
636
- /**
637
- * Converts a pattern string into a `new RegExp(...)` constructor call or a regex literal string.
638
- * Inline flags expressed as a `^(?im)` prefix are extracted and applied to the resulting expression.
639
- * Pass `null` as the second argument to emit a `/pattern/flags` literal instead.
640
- *
641
- * @example
642
- * ```ts
643
- * toRegExpString('^(?im)foo') // 'new RegExp("^foo", "im")'
644
- * toRegExpString('^(?im)foo', null) // '/^foo/im'
645
- * ```
646
- */
647
- function toRegExpString(text, func = "RegExp") {
648
- const raw = trimQuotes(text);
649
- const match = raw.match(/^\^(\(\?([igmsuy]+)\))/i);
650
- const replacementTarget = match?.[1] ?? "";
651
- const matchedFlags = match?.[2];
652
- const { source, flags } = toRegExp(raw.replace(/^\\?\//, "").replace(/\\?\/$/, "").replace(replacementTarget, ""), matchedFlags);
653
- if (func === null) return `/${source}/${flags}`;
654
- return `new ${func}(${JSON.stringify(source)}${flags ? `, ${JSON.stringify(flags)}` : ""})`;
655
- }
656
- // ../../internals/utils/src/codegen.ts
657
- const INDENT = " ";
658
- /**
659
- * Indents every non-empty line of `text` by one indent level, leaving blank lines empty.
551
+ * The schema a status occupies in the `<Name>Responses` record. A status that documents several
552
+ * content types becomes a `{ contentType; data }` union so the runtime can surface the negotiated type
553
+ * on `result.parsed`, while the standalone `<Name>StatusNNN` alias stays the plain body union that the
554
+ * query hooks and `result.data` use. With `streamData`, the stream variant of a 2xx status holds that schema, since the
555
+ * client returns its body unread.
660
556
  */
661
- function indentLines(text) {
662
- if (!text) return "";
663
- return text.split("\n").map((line) => line.trim() ? `${INDENT}${line}` : "").join("\n");
557
+ function buildResponseRecordEntry(node, res, resolver, streamData) {
558
+ const statusName = resolver.response.status(node, res.statusCode);
559
+ const variants = (res.content ?? []).filter((entry) => entry.schema);
560
+ if (variants.length <= 1) return ast.factory.createSchema({
561
+ type: "ref",
562
+ name: statusName
563
+ });
564
+ return ast.factory.createSchema({
565
+ type: "union",
566
+ members: resolveContentTypeVariants(variants, statusName).map((variant) => ast.factory.createSchema({
567
+ type: "object",
568
+ primitive: "object",
569
+ properties: [ast.factory.createProperty({
570
+ name: "contentType",
571
+ required: true,
572
+ schema: ast.factory.createSchema({
573
+ type: "enum",
574
+ enumValues: [ast.mediaType.getBase(variant)]
575
+ })
576
+ }), ast.factory.createProperty({
577
+ name: "data",
578
+ required: true,
579
+ schema: streamData && ast.http.status.isSuccess(res) && ast.mediaType.isStream(variant) ? streamData : ast.factory.createSchema({
580
+ type: "ref",
581
+ name: variant.name
582
+ })
583
+ })]
584
+ }))
585
+ });
664
586
  }
665
587
  /**
666
- * Renders an object key, quoting it with single quotes only when it is not a valid identifier.
667
- * Reserved words and globals (`name`, `class`, …) are valid bare keys and stay unquoted.
588
+ * Builds the per-status `<Name>Responses` record for an operation, referencing the already-resolved
589
+ * `<Name>StatusNNN` names. Shared by `@kubb/plugin-ts`'s `Responses` type and `@kubb/plugin-zod`'s
590
+ * inferred responses schema, so both emit the same shape from the same inputs.
668
591
  *
669
- * @example
670
- * ```ts
671
- * objectKey('name') // 'name'
672
- * objectKey('x-total') // "'x-total'"
673
- * ```
674
- */
675
- function objectKey(name) {
676
- return isIdentifier(name) ? name : singleQuote(name);
677
- }
678
- /**
679
- * Assembles a multi-line object literal from already-rendered `entries`, indenting each entry one
680
- * level and closing the brace at column zero. Entries that are themselves multi-line objects indent
681
- * cumulatively. Each entry ends with a trailing comma to match the formatter's multi-line style.
592
+ * Always emits the keyed record, even when an operation declares no responses. An operation with no
593
+ * responses renders as an empty object, which keeps every consumer's import (for example the axios
594
+ * SDK's `RequestResult<XResponses>`) resolvable instead of pointing at a missing export.
682
595
  *
683
- * @example
684
- * ```ts
685
- * buildObject(['id: z.number()', 'name: z.string()'])
686
- * // '{\n id: z.number(),\n name: z.string(),\n}'
687
- * ```
596
+ * `streamData` is the type of a stream variant's `data`, such as a `ReadableStream<Uint8Array>` reference.
688
597
  */
689
- function buildObject(entries) {
690
- if (entries.length === 0) return "{}";
691
- return `{\n${entries.map((entry) => `${indentLines(entry)},`).join("\n")}\n}`;
598
+ function buildResponses(node, resolver, { streamData } = {}) {
599
+ return ast.factory.createSchema({
600
+ type: "object",
601
+ primitive: "object",
602
+ properties: node.responses.map((res) => ast.factory.createProperty({
603
+ name: String(res.statusCode),
604
+ required: true,
605
+ schema: buildResponseRecordEntry(node, res, resolver, streamData)
606
+ }))
607
+ });
692
608
  }
609
+ // ../../internals/shared/src/adapter.ts
693
610
  /**
694
- * Assembles a bracketed list (array by default) from already-rendered `items`. Keeps everything on
695
- * one line when no item spans multiple lines, and otherwise puts each item on its own line, indented
696
- * one level with a trailing comma and the closing bracket at column zero. Used for member lists such
697
- * as `z.union([…])` and `z.array([…])`.
611
+ * Narrows the generic `Adapter` from a generator context to the OpenAPI adapter,
612
+ * so OAS-only options (`dateType`, `enums`) and the parsed `document` are typed.
698
613
  *
699
- * @example
700
- * ```ts
701
- * buildList(['z.string()', 'z.number()'])
702
- * // '[z.string(), z.number()]'
703
- * ```
704
- */
705
- function buildList(items, brackets = ["[", "]"]) {
706
- const [open, close] = brackets;
707
- if (items.length === 0) return `${open}${close}`;
708
- if (!items.some((item) => item.includes("\n"))) return `${open}${items.join(", ")}${close}`;
709
- return `${open}\n${items.map((item) => `${indentLines(item)},`).join("\n")}\n${close}`;
710
- }
711
- /**
712
- * Emits a lazy getter for a circular-ref property position, `get name() { return body }`. The key
713
- * is quoted only when it is not a valid identifier. Used by the string printers to defer evaluation
714
- * of a recursive schema until first access.
614
+ * Throws when a non-OAS adapter is configured, turning a silently wrong cast into a
615
+ * clear, actionable error at the point of use.
715
616
  *
716
617
  * @example
717
618
  * ```ts
718
- * lazyGetter({ name: 'parent', body: 'z.lazy(() => Pet)' })
719
- * // "get parent() { return z.lazy(() => Pet) }"
619
+ * const { dateType } = getOasAdapter(ctx.adapter).options
720
620
  * ```
721
621
  */
722
- function lazyGetter({ name, body }) {
723
- return `get ${objectKey(name)}() { return ${body} }`;
724
- }
725
- // ../../internals/utils/src/fs.ts
726
- /**
727
- * Builds a nested file path from a dotted name. Splits on dots that precede a letter
728
- * (so version numbers embedded in operationIds like `v2025.0` stay intact), camelCases
729
- * every earlier segment, applies `caseLast` to the final segment, and joins with `/`.
730
- *
731
- * Empty segments are dropped before joining. They arise when the name starts with a dot
732
- * followed by a letter (e.g. `..Schema` splits into `['..', 'Schema']` and `'..'` cases to
733
- * an empty string). Without this a leading `/` would form, which `path.resolve` reads as an
734
- * absolute path, letting generated files escape the configured output directory.
735
- *
736
- * @example Nested path from a dotted name
737
- * `toFilePath('pet.petId') // 'pet/petId'`
738
- *
739
- * @example PascalCase the final segment
740
- * `toFilePath('pet.Pet', pascalCase) // 'pet/Pet'`
741
- *
742
- * @example Suffix applied to the final segment only
743
- * `toFilePath('tag.tag', (part) => camelCase(part, { suffix: 'schema' })) // 'tag/tagSchema'`
744
- */
745
- function toFilePath(name, caseLast = camelCase) {
746
- const parts = name.split(/\.(?=[a-zA-Z])/);
747
- return parts.map((part, i) => i === parts.length - 1 ? caseLast(part) : camelCase(part)).filter(Boolean).join("/");
622
+ function getOasAdapter(adapter) {
623
+ if (adapter.name !== "oas") throw new Diagnostics.Error({
624
+ code: Diagnostics.code.adapterRequired,
625
+ severity: "error",
626
+ message: `Expected the OpenAPI adapter (adapterOas), but received "${adapter.name}".`,
627
+ help: "Set `adapter: adapterOas()` in kubb.config.ts.",
628
+ location: { kind: "config" }
629
+ });
630
+ return adapter;
748
631
  }
749
632
  // ../../internals/shared/src/resolver.ts
750
633
  /**
@@ -834,6 +717,13 @@ function createOperationResponseResolver() {
834
717
  name
835
718
  });
836
719
  },
720
+ headers(node, statusCode) {
721
+ const name = this.name(`${node.operationId} Status ${statusCode} Headers`);
722
+ return this.resolveName({
723
+ key: operationKey(node, "response", "headers", statusCode),
724
+ name
725
+ });
726
+ },
837
727
  body(node) {
838
728
  const name = this.name(`${node.operationId} Body`);
839
729
  return this.resolveName({
@@ -875,75 +765,7 @@ function createCasedFile(caseLast) {
875
765
  * Names of the schemas in a circular reference chain, computed once per build.
876
766
  */
877
767
  function getCircularNames(ctx) {
878
- return ctx.memo("circularNames:set", () => ast.findCircularSchemas(ctx.driver.inputNode?.schemas ?? []));
879
- }
880
- // ../../internals/shared/src/group.ts
881
- /**
882
- * Builds the `group` config a Kubb plugin passes to `ctx.setOptions`, applying the
883
- * shared default naming so every plugin groups output consistently:
884
- *
885
- * - `path` groups use the second path segment (`/pet/findByStatus` → `pet`).
886
- * - other groups use the camelCased group (`pet store` → `petStore`).
887
- *
888
- * A user-provided `group.name` always wins over the default namer, so callers stay in
889
- * control of their output folders. Returns `null` when grouping is disabled, matching the
890
- * per-plugin convention.
891
- *
892
- * @param group - The user-supplied group option, or `undefined` to disable grouping.
893
- *
894
- * @example
895
- * ```ts
896
- * createGroupConfig(group) // shared across every plugin
897
- * ```
898
- */
899
- function createGroupConfig(group) {
900
- if (!group) return null;
901
- const defaultName = (ctx) => {
902
- if (group.type === "path") return `${ctx.group.split("/")[1]}`;
903
- return camelCase(ctx.group);
904
- };
905
- return {
906
- ...group,
907
- name: group.name ? group.name : defaultName
908
- };
909
- }
910
- // ../../internals/shared/src/schemaTraversal.ts
911
- /**
912
- * Maps each property of an object schema to its transformed output. Pairs every result with the
913
- * original property so the printer keeps full control over modifiers, getters, and key syntax.
914
- *
915
- * @example
916
- * ```ts
917
- * const entries = mapSchemaProperties(node, (schema) => this.transform(schema))
918
- * // entries: [{ name: 'id', property, output: 'z.number()' }, ...]
919
- * ```
920
- */
921
- function mapSchemaProperties(node, transform) {
922
- return node.properties.map((property) => ({
923
- name: property.name,
924
- property,
925
- output: transform(property.schema)
926
- }));
927
- }
928
- /**
929
- * Maps each member of a union or intersection schema to its transformed output, pairing every
930
- * result with the original member.
931
- */
932
- function mapSchemaMembers(node, transform) {
933
- return (node.members ?? []).map((schema) => ({
934
- schema,
935
- output: transform(schema)
936
- }));
937
- }
938
- /**
939
- * Maps each item of an array or tuple schema to its transformed output, pairing every result with
940
- * the original item.
941
- */
942
- function mapSchemaItems(node, transform) {
943
- return (node.items ?? []).map((schema) => ({
944
- schema,
945
- output: transform(schema)
946
- }));
768
+ return ctx.memo("circularNames:set", () => ast.graph.getCircularNames(ctx.driver.inputNode?.schemas ?? []));
947
769
  }
948
770
  // src/components/Zod.tsx
949
771
  function Zod({ name, node, printer, inferTypeName, inferKind = "infer", typeGuards, isName, assertName, mini, cyclic, compile }) {
@@ -951,9 +773,9 @@ function Zod({ name, node, printer, inferTypeName, inferKind = "infer", typeGuar
951
773
  const printerImports = printer.drainImports();
952
774
  if (!output) return;
953
775
  const needsAnnotation = cyclic && node.type !== "object";
954
- const isBare = ast.isBareRef(node, printer.options.keysToOmit);
955
- const isCyclic = Boolean(cyclic || printer.options.cyclicSchemas && containsCircularRef(node, { circularSchemas: printer.options.cyclicSchemas }));
956
- const value = Boolean(compile) && !isBare && !isCyclic ? typeof compile === "object" && compile.strict ? `z.compile(${output}, { strict: true })` : `z.compile(${output})` : output;
776
+ const isAlias = ast.ref.isAlias(node, printer.options.keysToOmit);
777
+ const isCyclic = Boolean(cyclic || printer.options.cyclicSchemas && ast.graph.containsCircular(node, { circularSchemas: printer.options.cyclicSchemas }));
778
+ const value = Boolean(compile) && !isAlias && !isCyclic ? typeof compile === "object" && compile.strict ? `z.compile(${output}, { strict: true })` : `z.compile(${output})` : output;
957
779
  const targetType = inferTypeName ?? `z.infer<typeof ${name}>`;
958
780
  const shouldGenerateIs = isName && (typeof typeGuards === "object" ? typeGuards.is ?? true : Boolean(typeGuards));
959
781
  const shouldGenerateAssert = assertName && (typeof typeGuards === "object" ? typeGuards.assert ?? true : Boolean(typeGuards));
@@ -976,7 +798,7 @@ function Zod({ name, node, printer, inferTypeName, inferKind = "infer", typeGuar
976
798
  children: /* @__PURE__ */ jsx(Const, {
977
799
  export: true,
978
800
  name,
979
- type: needsAnnotation ? "z.ZodType" : void 0,
801
+ type: needsAnnotation ? mini ? "z.ZodMiniType" : "z.ZodType" : void 0,
980
802
  children: value
981
803
  })
982
804
  }),
@@ -1036,7 +858,7 @@ function shouldCoerce(coercion, type) {
1036
858
  */
1037
859
  function isClosedKeySchema(node) {
1038
860
  if (!node) return false;
1039
- const resolved = syncSchemaRef(node);
861
+ const resolved = ast.ref.mergeSchema(node);
1040
862
  if (resolved.type === "enum") return true;
1041
863
  if (resolved.type === "union") {
1042
864
  const members = resolved.members ?? [];
@@ -1046,12 +868,12 @@ function isClosedKeySchema(node) {
1046
868
  }
1047
869
  /**
1048
870
  * Whether the node is a plain inline object whose shape can be lifted into an `.extend({ … })`
1049
- * argument. Property counts, a catchall, `patternProperties`, or a nullable/optional wrapper cannot, so those stay
871
+ * argument. Input checks, a catchall, `patternProperties`, or a nullable/optional wrapper cannot, so those stay
1050
872
  * on `.and(…)`.
1051
873
  */
1052
874
  function isPlainInlineObject(node) {
1053
875
  if (node.type !== "object") return false;
1054
- if (node.minProperties !== void 0 || node.maxProperties !== void 0) return false;
876
+ if (hasInputChecks(node)) return false;
1055
877
  if (node.nullable || node.optional || node.nullish) return false;
1056
878
  const hasAdditionalProperties = node.additionalProperties !== void 0;
1057
879
  const hasPatternProperties = Boolean(node.patternProperties);
@@ -1062,21 +884,21 @@ function isPlainInlineObject(node) {
1062
884
  * `z.discriminatedUnion` option. Covers object nodes, `$ref`s that resolve to (or are still
1063
885
  * unresolved) objects, single-member unions of an object, and object-composable `allOf`.
1064
886
  *
1065
- * Property counts render as a pipe. A ref into `cyclicSchemas` renders as `z.lazy(…)`.
887
+ * Input checks, such as property counts and `conditionals`, render as a pipe. A ref into `cyclicSchemas` renders as `z.lazy(…)`.
1066
888
  */
1067
889
  function isObjectSchemaNode(node, cyclicSchemas) {
1068
890
  if (node.nullable || node.optional || node.nullish) return false;
891
+ if (hasInputChecks(node)) return false;
1069
892
  if (node.type === "object") {
1070
- if (node.minProperties !== void 0 || node.maxProperties !== void 0) return false;
1071
893
  const hasProperties = Boolean(node.properties?.length);
1072
894
  const hasTypedAdditionalProperties = Boolean(node.additionalProperties && node.additionalProperties !== true);
1073
895
  const hasPatternProperties = Boolean(node.patternProperties);
1074
896
  return !(!hasProperties && (hasTypedAdditionalProperties || hasPatternProperties || node.propertyNames));
1075
897
  }
1076
898
  if (node.type === "ref") {
1077
- const refName = ast.resolveRefName(node);
899
+ const refName = ast.ref.getName(node);
1078
900
  if (refName && cyclicSchemas?.has(refName)) return false;
1079
- const resolved = syncSchemaRef(node);
901
+ const resolved = ast.ref.mergeSchema(node);
1080
902
  return resolved.type === "ref" || isObjectSchemaNode(resolved, cyclicSchemas);
1081
903
  }
1082
904
  if (node.type === "union") {
@@ -1099,23 +921,23 @@ function preserveRequiredProperties(members) {
1099
921
  const hasRequiredProperty = ({ node, propertyName, seen = /* @__PURE__ */ new Set() }) => {
1100
922
  if (seen.has(node)) return false;
1101
923
  seen.add(node);
1102
- const resolved = syncSchemaRef(node);
1103
- const composition = ast.narrowSchema(resolved, ast.schemaTypes.intersection) ?? ast.narrowSchema(resolved, ast.schemaTypes.union);
924
+ const resolved = ast.ref.mergeSchema(node);
925
+ const composition = ast.schema.narrow(resolved, ast.schema.types.intersection) ?? ast.schema.narrow(resolved, ast.schema.types.union);
1104
926
  if (composition) {
1105
- if (composition.type === ast.schemaTypes.union && composition.members?.length !== 1) return false;
927
+ if (composition.type === ast.schema.types.union && composition.members?.length !== 1) return false;
1106
928
  return composition.members?.some((child) => hasRequiredProperty({
1107
929
  node: child,
1108
930
  propertyName,
1109
931
  seen
1110
932
  })) ?? false;
1111
933
  }
1112
- return ast.resolveSchemaProperties({
934
+ return ast.schema.findProperties({
1113
935
  node: resolved,
1114
936
  propertyName
1115
937
  }).some((property) => property.required !== false && !property.schema.optional && !property.schema.nullish);
1116
938
  };
1117
939
  return members.map((member, index) => {
1118
- const object = ast.narrowSchema(member, ast.schemaTypes.object);
940
+ const object = ast.schema.narrow(member, ast.schema.types.object);
1119
941
  if (!object || index === 0) return member;
1120
942
  return {
1121
943
  ...object,
@@ -1144,7 +966,7 @@ function preserveRequiredProperties(members) {
1144
966
  * representation, strings are quoted.
1145
967
  */
1146
968
  function formatDefault(value) {
1147
- if (typeof value === "string") return stringify(value);
969
+ if (typeof value === "string") return text.quote(value);
1148
970
  if (typeof value === "object" && value !== null) return JSON.stringify(value);
1149
971
  return String(value ?? "");
1150
972
  }
@@ -1159,16 +981,17 @@ function formatDefault(value) {
1159
981
  */
1160
982
  function defaultLiteral(node, value) {
1161
983
  if (value === null) return null;
1162
- const resolved = node ? syncSchemaRef(node) : void 0;
1163
- if (resolved && ast.narrowSchema(resolved, "bigint")) {
984
+ const resolved = node ? ast.ref.mergeSchema(node) : void 0;
985
+ if (resolved?.type === "blob") return null;
986
+ if (resolved && ast.schema.narrow(resolved, "bigint")) {
1164
987
  if (typeof value === "bigint") return `BigInt(${value})`;
1165
988
  if (typeof value === "number" && Number.isInteger(value)) return `BigInt(${value})`;
1166
989
  return null;
1167
990
  }
1168
- if (resolved && ast.narrowSchema(resolved, "array")) return Array.isArray(value) ? JSON.stringify(value) : null;
1169
- const enumNode = resolved ? ast.narrowSchema(resolved, "enum") : void 0;
991
+ if (resolved && ast.schema.narrow(resolved, "array")) return Array.isArray(value) ? JSON.stringify(value) : null;
992
+ const enumNode = resolved ? ast.schema.narrow(resolved, "enum") : void 0;
1170
993
  if (enumNode) {
1171
- const values = enumNode.namedEnumValues?.map((member) => member.value) ?? enumNode.enumValues ?? [];
994
+ const values = ast.schema.getLiterals(enumNode);
1172
995
  if (values.length) {
1173
996
  const match = values.find((member) => member === value || String(member) === String(value));
1174
997
  return match != null ? formatDefault(match) : null;
@@ -1237,13 +1060,13 @@ function patternKeySchemaMini({ patterns, regexType }) {
1237
1060
  * Build `.min()` / `.max()` / `.gt()` / `.lt()` constraint chains for numbers
1238
1061
  * using the standard chainable Zod v4 API.
1239
1062
  */
1240
- function numberConstraints({ min, max, exclusiveMinimum, exclusiveMaximum, multipleOf }) {
1063
+ function numberConstraints({ min, max, exclusive, multipleOf }, suffix = "") {
1241
1064
  return [
1242
- min !== void 0 ? `.min(${min})` : "",
1243
- max !== void 0 ? `.max(${max})` : "",
1244
- exclusiveMinimum !== void 0 ? `.gt(${exclusiveMinimum})` : "",
1245
- exclusiveMaximum !== void 0 ? `.lt(${exclusiveMaximum})` : "",
1246
- multipleOf !== void 0 ? `.multipleOf(${multipleOf})` : ""
1065
+ min !== void 0 ? `.min(${min}${suffix})` : "",
1066
+ max !== void 0 ? `.max(${max}${suffix})` : "",
1067
+ exclusive?.min !== void 0 ? `.gt(${exclusive.min}${suffix})` : "",
1068
+ exclusive?.max !== void 0 ? `.lt(${exclusive.max}${suffix})` : "",
1069
+ multipleOf !== void 0 ? `.multipleOf(${multipleOf}${suffix})` : ""
1247
1070
  ].join("");
1248
1071
  }
1249
1072
  /**
@@ -1258,36 +1081,71 @@ function lengthConstraints({ min, max, pattern, regexType }) {
1258
1081
  ].join("");
1259
1082
  }
1260
1083
  /**
1084
+ * Numeric bounds for a bigint: the inclusive, whole, safe bounds faker mocks against, and a whole, safe `multipleOf`.
1085
+ */
1086
+ function bigintConstraints({ min, max, exclusive, multipleOf }) {
1087
+ return {
1088
+ ...ast.schema.getNumberBounds({
1089
+ min,
1090
+ max,
1091
+ exclusive,
1092
+ integer: true
1093
+ }),
1094
+ multipleOf: Number.isSafeInteger(multipleOf) ? multipleOf : void 0
1095
+ };
1096
+ }
1097
+ /**
1261
1098
  * Build `.check(z.minimum(), z.maximum())` for `zod/mini` numeric constraints.
1262
1099
  */
1263
- function numberChecksMini({ min, max, exclusiveMinimum, exclusiveMaximum, multipleOf }) {
1264
- const checks = [];
1265
- if (min !== void 0) checks.push(`z.minimum(${min})`);
1266
- if (max !== void 0) checks.push(`z.maximum(${max})`);
1267
- if (exclusiveMinimum !== void 0) checks.push(`z.minimum(${exclusiveMinimum}, { exclusive: true })`);
1268
- if (exclusiveMaximum !== void 0) checks.push(`z.maximum(${exclusiveMaximum}, { exclusive: true })`);
1269
- if (multipleOf !== void 0) checks.push(`z.multipleOf(${multipleOf})`);
1100
+ function numberChecksMini({ min, max, exclusive, multipleOf }, suffix = "") {
1101
+ const checks = [
1102
+ min !== void 0 && `z.minimum(${min}${suffix})`,
1103
+ max !== void 0 && `z.maximum(${max}${suffix})`,
1104
+ exclusive?.min !== void 0 && `z.gt(${exclusive.min}${suffix})`,
1105
+ exclusive?.max !== void 0 && `z.lt(${exclusive.max}${suffix})`,
1106
+ multipleOf !== void 0 && `z.multipleOf(${multipleOf}${suffix})`
1107
+ ].filter(Boolean);
1270
1108
  return checks.length ? `.check(${checks.join(", ")})` : "";
1271
1109
  }
1272
1110
  /**
1273
1111
  * Build `.check(z.minLength(), z.maxLength(), z.regex())` for `zod/mini` length constraints.
1274
1112
  */
1275
1113
  function lengthChecksMini({ min, max, pattern, regexType }) {
1276
- const checks = [];
1277
- if (min !== void 0) checks.push(`z.minLength(${min})`);
1278
- if (max !== void 0) checks.push(`z.maxLength(${max})`);
1279
- if (pattern !== void 0) checks.push(`z.regex(${regexSource(pattern, regexType)})`);
1114
+ const checks = [
1115
+ min !== void 0 && `z.minLength(${min})`,
1116
+ max !== void 0 && `z.maxLength(${max})`,
1117
+ pattern !== void 0 && `z.regex(${regexSource(pattern, regexType)})`
1118
+ ].filter(Boolean);
1280
1119
  return checks.length ? `.check(${checks.join(", ")})` : "";
1281
1120
  }
1282
1121
  /**
1283
- * Checks property counts before object parsing strips unknown keys or inserts defaults.
1122
+ * Whether `propertyNames` needs its own check: `z.record` checks the keys of a map, but `z.object` and a record keyed by
1123
+ * `patternProperties` do not.
1124
+ */
1125
+ function hasKeyCheck(node) {
1126
+ if (node.type !== "object" || !node.propertyNames) return false;
1127
+ const hasTypedAdditionalProperties = Boolean(node.additionalProperties && node.additionalProperties !== true);
1128
+ return node.properties.length > 0 || Boolean(node.patternProperties) && !hasTypedAdditionalProperties;
1129
+ }
1130
+ /**
1131
+ * Whether the node prints inside `z.pipe(z.unknown().check(…), …)`, see {@link inputChecks}.
1284
1132
  */
1285
- function objectConstraints({ value, node }) {
1286
- if (node.type !== "object") return value;
1287
- const checks = [];
1133
+ function hasInputChecks(node) {
1134
+ return Boolean(node.conditionals?.length) || node.type === "object" && (node.min !== void 0 || node.max !== void 0) || hasKeyCheck(node);
1135
+ }
1136
+ /**
1137
+ * Checks the raw input, before parsing strips unknown keys or inserts defaults: property counts, key names, then the
1138
+ * printed `conditionals`.
1139
+ */
1140
+ function inputChecks({ value, node, keySchema, conditionals }) {
1288
1141
  const skip = "typeof value !== \"object\" || value === null || Array.isArray(value)";
1289
- if (node.minProperties !== void 0) checks.push(`z.refine(value => ${skip} || Object.keys(value).length >= ${node.minProperties}, { message: "Object must have at least ${node.minProperties} properties" })`);
1290
- if (node.maxProperties !== void 0) checks.push(`z.refine(value => ${skip} || Object.keys(value).length <= ${node.maxProperties}, { message: "Object must have at most ${node.maxProperties} properties" })`);
1142
+ const { min, max } = node.type === "object" ? node : {};
1143
+ const checks = [
1144
+ min !== void 0 && `z.refine(value => ${skip} || Object.keys(value).length >= ${min}, { message: "Object must have at least ${min} properties" })`,
1145
+ max !== void 0 && `z.refine(value => ${skip} || Object.keys(value).length <= ${max}, { message: "Object must have at most ${max} properties" })`,
1146
+ keySchema && `z.refine(value => ${skip} || Object.keys(value).every((key) => ${keySchema}.safeParse(key).success), { message: "Object keys must match the property names schema" })`,
1147
+ ...conditionals
1148
+ ].filter(Boolean);
1291
1149
  return checks.length ? `z.pipe(z.unknown().check(${checks.join(", ")}), ${value})` : value;
1292
1150
  }
1293
1151
  /**
@@ -1303,8 +1161,8 @@ function applyModifiers({ value, schema, nullable, optional, nullish, defaultVal
1303
1161
  })();
1304
1162
  const literal = defaultValue !== void 0 ? defaultLiteral(schema, defaultValue) : null;
1305
1163
  const withDefault = literal !== null ? `${withModifier}.default(${literal})` : withModifier;
1306
- const withDescription = description ? `${withDefault}.describe(${stringify(description)})` : withDefault;
1307
- return examples?.length ? `${withDescription}.meta({ examples: [${examples.map(formatDefault).join(", ")}] })` : withDescription;
1164
+ const withDescription = description ? `${withDefault}.describe(${text.quote(description)})` : withDefault;
1165
+ return examples?.length ? `${withDescription}.meta({ examples: [${examples.map((example) => example === null ? "null" : formatDefault(example)).join(", ")}] })` : withDescription;
1308
1166
  }
1309
1167
  function modifierDepth(schema) {
1310
1168
  if (schema.nullish || schema.nullable && schema.optional) return 2;
@@ -1322,7 +1180,7 @@ function modifierDepth(schema) {
1322
1180
  * `.unwrap()`, so its printer passes `._zod.def.innerType` as the `step`.
1323
1181
  */
1324
1182
  function omitUnwrapChain(node, step = ".unwrap()") {
1325
- const ref = ast.narrowSchema(node, "ref");
1183
+ const ref = ast.schema.narrow(node, "ref");
1326
1184
  if (!ref) return "";
1327
1185
  const target = ref.schema ?? ref;
1328
1186
  const depth = modifierDepth(target) + (target.default !== void 0 ? 1 : 0);
@@ -1344,7 +1202,7 @@ function applyMiniModifiers({ value, schema, nullable, optional, nullish, defaul
1344
1202
  /**
1345
1203
  * Calls for string `format`s with a dedicated Zod constructor; a `Map` so `constructor` cannot hit `Object.prototype`.
1346
1204
  */
1347
- const STRING_FORMAT_CALLS = /* @__PURE__ */ new Map([
1205
+ const stringFormatCalls = /* @__PURE__ */ new Map([
1348
1206
  ["byte", "z.base64()"],
1349
1207
  ["base64", "z.base64()"],
1350
1208
  ["base64url", "z.base64url()"],
@@ -1355,6 +1213,16 @@ const STRING_FORMAT_CALLS = /* @__PURE__ */ new Map([
1355
1213
  ["hostname", "z.hostname()"]
1356
1214
  ]);
1357
1215
  /**
1216
+ * Calls for a string's `contentEncoding` with a dedicated Zod constructor.
1217
+ */
1218
+ const contentEncodingCalls = /* @__PURE__ */ new Map([["base64", "z.base64()"], ["base64url", "z.base64url()"]]);
1219
+ /**
1220
+ * The check for a string that holds a JSON document: it must parse, and the parsed value must pass `schema`.
1221
+ */
1222
+ function jsonContentCheck(schema) {
1223
+ return `(value) => { try { return ${schema}.safeParse(JSON.parse(value)).success } catch { return false } }, { message: "String must hold JSON that matches its content schema" }`;
1224
+ }
1225
+ /**
1358
1226
  * Marks an inline `oneOf` object strict, so `object()` builds `z.strictObject(...)` and no eager `.shape` read hits a deferred getter.
1359
1227
  */
1360
1228
  function strictOneOfNode(node) {
@@ -1367,13 +1235,14 @@ function strictOneOfNode(node) {
1367
1235
  * The constraint chain an `allOf` member adds to a bare string, number or array; each printer passes its own renderers.
1368
1236
  */
1369
1237
  function getMemberConstraint({ member, regexType, lengths, numbers }) {
1238
+ if (hasInputChecks(member) || ast.schema.narrow(member, "array")?.contains) return void 0;
1370
1239
  if (member.primitive === "string") return lengths({
1371
- ...ast.narrowSchema(member, "string") ?? {},
1240
+ ...ast.schema.narrow(member, "string") ?? {},
1372
1241
  regexType
1373
1242
  }) || void 0;
1374
- if (member.primitive === "number" || member.primitive === "integer") return numbers(ast.narrowSchema(member, "number") ?? ast.narrowSchema(member, "integer") ?? {}) || void 0;
1243
+ if (member.primitive === "number" || member.primitive === "integer") return numbers(ast.schema.narrow(member, "number") ?? ast.schema.narrow(member, "integer") ?? {}) || void 0;
1375
1244
  if (member.primitive === "array") return lengths({
1376
- ...ast.narrowSchema(member, "array") ?? {},
1245
+ ...ast.schema.narrow(member, "array") ?? {},
1377
1246
  regexType
1378
1247
  }) || void 0;
1379
1248
  }
@@ -1381,24 +1250,26 @@ function getMemberConstraint({ member, regexType, lengths, numbers }) {
1381
1250
  * Builds the `{ key: value, … }` shape shared by the `z.object(...)` and `.extend(...)` renderings.
1382
1251
  */
1383
1252
  function buildObjectShape(ctx, node, renderProperty) {
1384
- const objectNode = ast.narrowSchema(node, "object");
1253
+ const objectNode = ast.schema.narrow(node, "object");
1385
1254
  if (!objectNode) return "{}";
1386
- const isCyclic = (schema) => ctx.options.cyclicSchemas != null && containsCircularRef(schema, { circularSchemas: ctx.options.cyclicSchemas });
1387
- return buildObject(mapSchemaProperties(objectNode, (schema) => {
1255
+ const isCyclic = (schema) => ctx.options.cyclicSchemas != null && ast.graph.containsCircular(schema, { circularSchemas: ctx.options.cyclicSchemas });
1256
+ const transformed = objectNode.properties.map(({ schema }) => {
1388
1257
  const hasSelfRef = isCyclic(schema);
1389
1258
  const savedCyclicSchemas = ctx.options.cyclicSchemas;
1390
1259
  if (hasSelfRef) ctx.options.cyclicSchemas = void 0;
1391
1260
  const baseOutput = ctx.transform(schema) ?? ctx.transform(ast.factory.createSchema({ type: "unknown" }));
1392
1261
  if (hasSelfRef) ctx.options.cyclicSchemas = savedCyclicSchemas;
1393
1262
  return baseOutput;
1394
- }).map(({ name, property, output: value }) => {
1395
- const { schema } = property;
1263
+ });
1264
+ return buildObject(objectNode.properties.map((property, index) => {
1265
+ const { name, schema } = property;
1266
+ const value = transformed[index];
1396
1267
  return renderProperty({
1397
1268
  name,
1398
1269
  value,
1399
1270
  schema,
1400
1271
  required: property.required,
1401
- meta: syncSchemaRef(schema),
1272
+ meta: ast.ref.mergeSchema(schema),
1402
1273
  cyclic: isCyclic(schema)
1403
1274
  });
1404
1275
  }));
@@ -1410,14 +1281,14 @@ function printWithOmit(ctx, node, modify, mini = false) {
1410
1281
  const { keysToOmit } = ctx.options;
1411
1282
  const transformed = ctx.transform(node);
1412
1283
  if (!transformed) return null;
1413
- const meta = syncSchemaRef(node);
1284
+ const meta = ast.ref.mergeSchema(node);
1414
1285
  return modify((() => {
1415
1286
  if (!keysToOmit?.length || meta.primitive !== "object" || meta.type === "union" && meta.discriminatorPropertyName) return transformed;
1416
1287
  const unwrap = omitUnwrapChain(node, mini ? "._zod.def.innerType" : ".unwrap()");
1417
1288
  const mask = `{ ${keysToOmit.map((k) => `"${k}": true`).join(", ")} }`;
1418
1289
  const omit = (target) => {
1419
1290
  const inner = `${target}${unwrap}`;
1420
- const hasObjectConstraints = meta.type === "object" && (meta.minProperties !== void 0 || meta.maxProperties !== void 0);
1291
+ const hasObjectConstraints = hasInputChecks(meta);
1421
1292
  const pipe = mini ? `${inner}._zod.def` : inner;
1422
1293
  const object = hasObjectConstraints ? `${pipe}.out` : inner;
1423
1294
  const omitted = mini ? `z.omit(${object}, ${mask})` : `${object}.omit(${mask})`;
@@ -1436,7 +1307,7 @@ function printWithOmit(ctx, node, modify, mini = false) {
1436
1307
  function getLiteralUnion({ node, options }) {
1437
1308
  if (node.strategy === "one" && options.oneOfType === "xor" || options.nodes?.enum) return void 0;
1438
1309
  const values = (node.members ?? []).map((member) => {
1439
- const enumNode = ast.narrowSchema(member, ast.schemaTypes.enum);
1310
+ const enumNode = ast.schema.narrow(member, ast.schema.types.enum);
1440
1311
  return enumNode && !enumNode.name && !enumNode.nullable && !enumNode.namedEnumValues && enumNode.enumValues?.length === 1 ? enumNode.enumValues[0] : void 0;
1441
1312
  });
1442
1313
  if (!values.length || !values.every((value) => value !== void 0 && value !== null)) return void 0;
@@ -1449,19 +1320,26 @@ function getLiteralUnion({ node, options }) {
1449
1320
  * Whether a discriminated union member's discriminator property prints as a coerced literal union.
1450
1321
  */
1451
1322
  function hasCoercedDiscriminator({ member, propertyName, options }) {
1452
- return ast.resolveSchemaProperties({
1323
+ return ast.schema.findProperties({
1453
1324
  node: member,
1454
1325
  propertyName
1455
1326
  }).some(({ schema }) => {
1456
- const resolved = syncSchemaRef(schema);
1327
+ const resolved = ast.ref.mergeSchema(schema);
1457
1328
  return resolved.type === "union" && !!getLiteralUnion({
1458
1329
  node: resolved,
1459
1330
  options
1460
1331
  })?.coerce;
1461
1332
  });
1462
1333
  }
1334
+ function hasPlainDiscriminator({ member, propertyName }) {
1335
+ const schemas = ast.schema.findProperties({
1336
+ node: member,
1337
+ propertyName
1338
+ });
1339
+ return schemas.length > 0 && !schemas.some(({ schema }) => ast.schema.getLiterals(ast.ref.mergeSchema(schema)).length > 0);
1340
+ }
1463
1341
  /**
1464
- * Builds the scalar handlers both dialects share. They never recurse into children. The temporal
1342
+ * Builds the scalar handlers both dialects share. Only a string with a `contentSchema` recurses into a child. The temporal
1465
1343
  * scalars and `bigint` differ per dialect, so each printer adds its own.
1466
1344
  */
1467
1345
  function buildScalarNodes(dialect) {
@@ -1474,18 +1352,20 @@ function buildScalarNodes(dialect) {
1474
1352
  null: () => "z.null()",
1475
1353
  string(node) {
1476
1354
  const coerce = shouldCoerce(this.options.coercion, "strings");
1477
- const call = coerce ? void 0 : STRING_FORMAT_CALLS.get(node.format);
1478
- if (call) return `${call}${dialect.lengths({
1479
- ...node,
1480
- regexType: this.options.regexType
1481
- })}`;
1482
- const base = coerce ? "z.coerce.string()" : "z.string()";
1355
+ const base = (coerce ? void 0 : stringFormatCalls.get(node.format) ?? contentEncodingCalls.get(node.content?.encoding?.toLowerCase())) ?? (coerce ? "z.coerce.string()" : "z.string()");
1483
1356
  const pattern = node.pattern ?? integerFormatPattern(node.format);
1484
- return `${base}${dialect.lengths({
1357
+ const value = `${base}${dialect.lengths({
1485
1358
  ...node,
1486
1359
  pattern,
1487
1360
  regexType: this.options.regexType
1488
1361
  })}`;
1362
+ const contentNode = ast.schema.getEmbeddedJson(node);
1363
+ if (!contentNode) return value;
1364
+ const direction = this.options.direction;
1365
+ this.options.direction = "decode";
1366
+ const contentSchema = this.transform(contentNode);
1367
+ this.options.direction = direction;
1368
+ return contentSchema ? dialect.refine(value, jsonContentCheck(contentSchema)) : value;
1489
1369
  },
1490
1370
  number(node) {
1491
1371
  return `${shouldCoerce(this.options.coercion, "numbers") ? "z.coerce.number()" : "z.number()"}${dialect.numbers(node)}`;
@@ -1518,7 +1398,7 @@ function buildScalarNodes(dialect) {
1518
1398
  ipv6: () => "z.ipv6()",
1519
1399
  blob: () => "z.instanceof(Blob)",
1520
1400
  enum(node) {
1521
- const nonNullValues = (node.namedEnumValues?.map((v) => v.value) ?? node.enumValues ?? []).filter((v) => v !== null);
1401
+ const nonNullValues = ast.schema.getLiterals(node).filter((v) => v !== null);
1522
1402
  if (node.namedEnumValues?.length) {
1523
1403
  const literals = nonNullValues.map((v) => buildLiteral(v));
1524
1404
  if (literals.length === 1) return literals[0];
@@ -1537,11 +1417,12 @@ function buildScalarNodes(dialect) {
1537
1417
  */
1538
1418
  function buildZodNodes(dialect, cyclicSchemas) {
1539
1419
  const unionsWithSiblings = /* @__PURE__ */ new WeakSet();
1420
+ const printNullable = (schema, output) => output && schema.nullable ? dialect.nullable(output) : output;
1540
1421
  return {
1541
1422
  ...buildScalarNodes(dialect),
1542
1423
  ref(node) {
1543
1424
  if (!node.name) return null;
1544
- const refName = ast.resolveRefName(node);
1425
+ const refName = ast.ref.getName(node);
1545
1426
  if (!refName) return null;
1546
1427
  const useInputVariant = node.ref != null && (dialect.useInputVariant?.(node, this.options) ?? false);
1547
1428
  const resolvedName = node.ref ? useInputVariant ? this.options.resolver?.schema.inputName(refName) ?? refName : this.options.resolver?.name(refName) ?? refName : node.name;
@@ -1556,47 +1437,50 @@ function buildZodNodes(dialect, cyclicSchemas) {
1556
1437
  const isStrict = node.additionalProperties === false && patterns.length === 0;
1557
1438
  const isLoose = node.additionalProperties === true;
1558
1439
  const objectBase = `${isStrict ? "z.strictObject" : isLoose ? "z.looseObject" : "z.object"}(${dialect.objectShape(this, node)})`;
1559
- return objectConstraints({
1560
- value: (() => {
1561
- const recordFn = isClosedKeySchema(propertyNamesNode) ? "z.partialRecord" : "z.record";
1562
- if (node.additionalProperties && node.additionalProperties !== true) {
1563
- const catchallType = this.transform(node.additionalProperties);
1564
- if (entries.length === 0) return catchallType ? `${recordFn}(${propertyNamesKeySchema || "z.string()"}, ${catchallType})` : objectBase;
1565
- return catchallType ? dialect.catchall(objectBase, catchallType) : objectBase;
1566
- }
1567
- if (entries.length === 0 && patterns.length === 0 && propertyNamesKeySchema && !isStrict) {
1568
- const unknownType = this.transform(ast.factory.createSchema({ type: ast.schemaTypes.unknown }));
1569
- return `${recordFn}(${propertyNamesKeySchema}, ${unknownType})`;
1570
- }
1571
- if (node.additionalProperties === true) return objectBase;
1572
- if (isStrict) return objectBase;
1573
- if (patterns.length > 0) {
1574
- const values = patterns.map(([, valueSchema]) => {
1575
- const valueType = this.transform(valueSchema) ?? this.transform(ast.factory.createSchema({ type: ast.schemaTypes.unknown }));
1576
- return valueSchema.nullable ? dialect.nullable(valueType) : valueType;
1577
- });
1578
- const distinct = [...new Set(values)];
1579
- const value = distinct.length === 1 ? distinct[0] : `z.union([${distinct.join(", ")}])`;
1580
- if (entries.length > 0) return dialect.catchall(objectBase, value);
1581
- return `z.record(${dialect.patternKey({
1582
- patterns: patterns.map(([pattern]) => pattern),
1583
- regexType: this.options.regexType
1584
- })}, ${value})`;
1585
- }
1586
- return objectBase;
1587
- })(),
1588
- node
1589
- });
1440
+ return (() => {
1441
+ const recordFn = isClosedKeySchema(propertyNamesNode) ? "z.partialRecord" : "z.record";
1442
+ if (node.additionalProperties && node.additionalProperties !== true) {
1443
+ const catchallType = this.transform(node.additionalProperties);
1444
+ if (entries.length === 0) return catchallType ? `${recordFn}(${propertyNamesKeySchema || "z.string()"}, ${catchallType})` : objectBase;
1445
+ return catchallType ? dialect.catchall(objectBase, catchallType) : objectBase;
1446
+ }
1447
+ if (entries.length === 0 && patterns.length === 0 && propertyNamesKeySchema && !isStrict) {
1448
+ const unknownType = this.transform(ast.factory.createSchema({ type: ast.schema.types.unknown }));
1449
+ return `${recordFn}(${propertyNamesKeySchema}, ${unknownType})`;
1450
+ }
1451
+ if (node.additionalProperties === true) return objectBase;
1452
+ if (isStrict) return objectBase;
1453
+ if (patterns.length > 0) {
1454
+ const values = patterns.map(([, valueSchema]) => {
1455
+ const valueType = this.transform(valueSchema) ?? this.transform(ast.factory.createSchema({ type: ast.schema.types.unknown }));
1456
+ return valueSchema.nullable ? dialect.nullable(valueType) : valueType;
1457
+ });
1458
+ const distinct = [...new Set(values)];
1459
+ const value = distinct.length === 1 ? distinct[0] : `z.union([${distinct.join(", ")}])`;
1460
+ if (entries.length > 0) return dialect.catchall(objectBase, value);
1461
+ return `z.record(${dialect.patternKey({
1462
+ patterns: patterns.map(([pattern]) => pattern),
1463
+ regexType: this.options.regexType
1464
+ })}, ${value})`;
1465
+ }
1466
+ return objectBase;
1467
+ })();
1590
1468
  },
1591
1469
  array(node) {
1592
- const base = `z.array(${mapSchemaItems(node, (item) => this.transform(item)).map(({ output }) => output).filter(Boolean).join(", ") || this.transform(ast.factory.createSchema({ type: ast.schemaTypes.unknown }))})${dialect.lengths({
1470
+ const base = `z.array(${(node.items ?? []).map((item) => printNullable(item, this.transform(item))).filter(Boolean).join(", ") || this.transform(ast.factory.createSchema({ type: ast.schema.types.unknown }))})${dialect.lengths({
1593
1471
  ...node,
1594
1472
  regexType: this.options.regexType
1595
1473
  })}`;
1596
- return node.unique ? dialect.unique(base) : base;
1474
+ const unique = node.unique ? dialect.unique(base) : base;
1475
+ const contains = node.contains && containsCheck(this, node.contains);
1476
+ return contains ? dialect.refine(unique, contains) : unique;
1597
1477
  },
1598
1478
  tuple(node) {
1599
- return `z.tuple(${buildList(mapSchemaItems(node, (item) => this.transform(item)).map(({ output }) => output).filter(Boolean))})`;
1479
+ const items = (node.items ?? []).map((item) => printNullable(item, this.transform(item))).filter(Boolean);
1480
+ const rest = node.rest ? this.transform(node.rest) : void 0;
1481
+ const tuple = `z.tuple(${buildList(items)}${rest ? `, ${rest}` : ""})`;
1482
+ const contains = node.contains && containsCheck(this, node.contains);
1483
+ return contains ? dialect.refine(tuple, contains) : tuple;
1600
1484
  },
1601
1485
  union(node) {
1602
1486
  const literalUnion = getLiteralUnion({
@@ -1609,24 +1493,30 @@ function buildZodNodes(dialect, cyclicSchemas) {
1609
1493
  }
1610
1494
  const nodeMembers = node.members ?? [];
1611
1495
  const { discriminatorPropertyName } = node;
1612
- const isDiscriminated = !!discriminatorPropertyName && nodeMembers.every((m) => isObjectSchemaNode(m, cyclicSchemas)) && !nodeMembers.some((m) => hasCoercedDiscriminator({
1496
+ const isDiscriminated = !!discriminatorPropertyName && nodeMembers.every((m) => isObjectSchemaNode(m, cyclicSchemas) && !hasPlainDiscriminator({
1497
+ member: m,
1498
+ propertyName: discriminatorPropertyName
1499
+ })) && !nodeMembers.some((m) => hasCoercedDiscriminator({
1613
1500
  member: m,
1614
1501
  propertyName: discriminatorPropertyName,
1615
1502
  options: this.options
1616
1503
  }));
1617
1504
  const isXor = node.strategy === "one" && !isDiscriminated && this.options.oneOfType === "xor";
1618
- const isStrict = node.strategy === "one" && !isXor && !unionsWithSiblings.has(node);
1619
- const members = mapSchemaMembers(node, (memberNode) => this.transform(isStrict ? strictOneOfNode(memberNode) : memberNode)).map(({ schema, output }) => output && isStrict && dialect.strictMember ? dialect.strictMember(output, schema, cyclicSchemas) : output).filter(Boolean);
1505
+ const isStrict = node.strategy === "one" && !isXor && !isDiscriminated && !unionsWithSiblings.has(node);
1506
+ const members = (node.members ?? []).map((memberNode) => {
1507
+ const output = this.transform(isStrict ? strictOneOfNode(memberNode) : memberNode);
1508
+ return output && isStrict && dialect.strictMember ? dialect.strictMember(output, memberNode, cyclicSchemas) : output;
1509
+ }).map((output, index) => printNullable(nodeMembers[index], output)).filter(Boolean);
1620
1510
  if (members.length === 0) return "";
1621
1511
  if (members.length === 1) return members[0];
1622
- if (isDiscriminated) return `z.discriminatedUnion(${stringify(node.discriminatorPropertyName)}, ${buildList(members)})`;
1512
+ if (isDiscriminated) return `z.discriminatedUnion(${text.quote(node.discriminatorPropertyName)}, ${buildList(members)})`;
1623
1513
  if (isXor) return `z.xor(${buildList(members)})`;
1624
1514
  return `z.union(${buildList(members)})`;
1625
1515
  },
1626
1516
  intersection(node) {
1627
1517
  const members = node.members ?? [];
1628
1518
  if (members.length === 0) return "";
1629
- const hasSiblingProperties = members.some((member) => member.type === ast.schemaTypes.object && member.properties.length > 0);
1519
+ const hasSiblingProperties = members.some((member) => member.type === ast.schema.types.object && member.properties.length > 0);
1630
1520
  const transform = (member) => {
1631
1521
  if (!hasSiblingProperties) return this.transform(member);
1632
1522
  unionsWithSiblings.add(member);
@@ -1640,7 +1530,18 @@ function buildZodNodes(dialect, cyclicSchemas) {
1640
1530
  if (!first) return "";
1641
1531
  const firstBase = transform(first);
1642
1532
  if (!firstBase) return "";
1643
- if (rest.length > 0 && isObjectComposableIntersection(node, cyclicSchemas)) return preserveRequiredProperties(members).slice(1).reduce((acc, member) => dialect.extend(acc, dialect.objectShape(this, member)), firstBase);
1533
+ if (rest.length > 0 && isObjectComposableIntersection(node, cyclicSchemas)) {
1534
+ const discriminator = ast.schema.narrow(first, ast.schema.types.union)?.discriminatorPropertyName;
1535
+ return preserveRequiredProperties(members).slice(1).reduce((acc, member) => {
1536
+ const object = ast.schema.narrow(member, ast.schema.types.object);
1537
+ if (!discriminator || !object?.properties.some((property) => property.name === discriminator)) return dialect.extend(acc, dialect.objectShape(this, member));
1538
+ const properties = object.properties.filter((property) => property.name !== discriminator);
1539
+ return properties.length ? dialect.extend(acc, dialect.objectShape(this, {
1540
+ ...object,
1541
+ properties
1542
+ })) : acc;
1543
+ }, firstBase);
1544
+ }
1644
1545
  return rest.reduce((acc, member) => {
1645
1546
  const constraint = getMemberConstraint({
1646
1547
  member,
@@ -1655,15 +1556,57 @@ function buildZodNodes(dialect, cyclicSchemas) {
1655
1556
  }
1656
1557
  };
1657
1558
  }
1559
+ function containsCheck(ctx, { schema, min = 1, max }) {
1560
+ if (min === 0 && max === void 0) return null;
1561
+ const countBounds = [`count >= ${min}`, max !== void 0 && `count <= ${max}`].filter(Boolean).join(" && ");
1562
+ const message = max !== void 0 ? `Array must contain ${min} to ${max} matching items` : `Array must contain at least ${min} matching items`;
1563
+ return `(items) => { const count = items.filter((item) => ${ctx.transform(schema) ?? "z.unknown()"}.safeParse(item).success).length; return ${countBounds} }, { message: ${text.quote(message)} }`;
1564
+ }
1565
+ function conditionExpression(ctx, schema) {
1566
+ if (!schema) return "true";
1567
+ const object = ast.schema.narrow(schema, ast.schema.types.object);
1568
+ const keyOnlyNames = object?.properties.filter((property) => property.required && property.schema.type === ast.schema.types.unknown).map((property) => property.name) ?? [];
1569
+ const restSchema = object ? {
1570
+ ...object,
1571
+ properties: object.properties.filter((property) => !keyOnlyNames.includes(property.name))
1572
+ } : schema;
1573
+ const isOnlyKeys = keyOnlyNames.length > 0 && restSchema.type === "object" && restSchema.properties.length === 0 && restSchema.additionalProperties === void 0 && !restSchema.patternProperties && !restSchema.propertyNames && !hasInputChecks(restSchema);
1574
+ return [keyOnlyNames.length > 0 && `typeof value === "object" && value !== null && ${JSON.stringify(keyOnlyNames)}.every((key) => key in value)`, !isOnlyKeys && `${ctx.transform(restSchema) ?? "z.unknown()"}.safeParse(value).success`].filter(Boolean).join(" && ");
1575
+ }
1576
+ /**
1577
+ * Wraps each handler so its output first checks the raw input, see {@link inputChecks}.
1578
+ */
1579
+ function withInputChecks(nodes) {
1580
+ const wrapHandler = (type) => {
1581
+ const handler = nodes[type];
1582
+ if (!handler) return [];
1583
+ const checkedHandler = function(node) {
1584
+ const output = handler.call(this, node);
1585
+ if (!output || !hasInputChecks(node)) return output;
1586
+ return inputChecks({
1587
+ value: output,
1588
+ node,
1589
+ keySchema: hasKeyCheck(node) ? this.transform(node.propertyNames) : void 0,
1590
+ conditionals: (node.conditionals ?? []).map((entry) => `z.refine((value) => (${conditionExpression(this, entry.if)} ? ${conditionExpression(this, entry.then)} : ${conditionExpression(this, entry.else)}), { message: "Value does not meet a conditional schema" })`)
1591
+ });
1592
+ };
1593
+ return [[type, checkedHandler]];
1594
+ };
1595
+ return Object.fromEntries(Object.values(ast.schema.types).flatMap(wrapHandler));
1596
+ }
1658
1597
  // src/printers/printerZod.ts
1659
1598
  function strictOneOfMember(member, node, cyclicSchemas) {
1660
- if (node.type === ast.schemaTypes.ref) {
1599
+ if (node.type === ast.schema.types.ref) {
1661
1600
  if (member.startsWith("z.lazy(")) return member;
1662
- const refName = ast.resolveRefName(node);
1601
+ const refName = ast.ref.getName(node);
1663
1602
  if (refName && cyclicSchemas?.has(refName)) return member;
1664
- const schema = syncSchemaRef(node);
1603
+ const schema = ast.ref.mergeSchema(node);
1665
1604
  if (schema.nullable || schema.optional || node.nullable || node.optional) return member;
1666
- if (schema.type === ast.schemaTypes.object && (schema.additionalProperties === void 0 || schema.additionalProperties === false)) return `${member}.strict()`;
1605
+ if (schema.type === ast.schema.types.object && (schema.additionalProperties === void 0 || schema.additionalProperties === false)) {
1606
+ const checks = [node, node.schema].filter((entry) => entry && hasInputChecks(entry)).length;
1607
+ if (checks === 2) return member;
1608
+ return checks ? `z.pipe(${member}.in, ${member}.out.strict())` : `${member}.strict()`;
1609
+ }
1667
1610
  }
1668
1611
  return member;
1669
1612
  }
@@ -1679,7 +1622,7 @@ function buildZodObjectShape(ctx, node) {
1679
1622
  optional: schema.optional || required === false,
1680
1623
  nullish: schema.nullish,
1681
1624
  defaultValue: meta.default,
1682
- description: schema.type !== ast.schemaTypes.ref && meta.type === ast.schemaTypes.ref ? void 0 : meta.description,
1625
+ description: schema.type !== ast.schema.types.ref && meta.type === ast.schema.types.ref ? void 0 : meta.description,
1683
1626
  examples: meta.examples
1684
1627
  });
1685
1628
  if (!cyclic) return `${objectKey(name)}: ${body}`;
@@ -1700,21 +1643,22 @@ function buildZodObjectShape(ctx, node) {
1700
1643
  function recursiveGetterType({ ctx, schema, required }) {
1701
1644
  function baseType(schema) {
1702
1645
  if (ctx.options.nodes?.[schema.type]) return void 0;
1703
- if (schema.type === ast.schemaTypes.ref) {
1646
+ if (schema.type === ast.schema.types.ref) {
1704
1647
  const saved = ctx.options.cyclicSchemas;
1705
1648
  ctx.options.cyclicSchemas = void 0;
1706
1649
  const name = ctx.transform(schema);
1707
1650
  ctx.options.cyclicSchemas = saved;
1708
1651
  return name ? `typeof ${name}` : void 0;
1709
1652
  }
1710
- if (schema.type === ast.schemaTypes.array && schema.items?.length === 1) {
1653
+ if (schema.type === ast.schema.types.array && schema.items?.length === 1) {
1711
1654
  const item = baseType(schema.items[0]);
1712
- return item ? `z.ZodArray<${item}>` : void 0;
1655
+ if (!item) return void 0;
1656
+ return `z.ZodArray<${schema.items[0].nullable ? `z.ZodNullable<${item}>` : item}>`;
1713
1657
  }
1714
1658
  }
1715
1659
  const base = baseType(schema);
1716
1660
  if (!base) return void 0;
1717
- const meta = syncSchemaRef(schema);
1661
+ const meta = ast.ref.mergeSchema(schema);
1718
1662
  const optional = schema.optional || required === false;
1719
1663
  const wrapped = (() => {
1720
1664
  if (schema.nullish || meta.nullable && optional) return `z.ZodOptional<z.ZodNullable<${base}>>`;
@@ -1729,12 +1673,12 @@ function recursiveGetterType({ ctx, schema, required }) {
1729
1673
  * `direction` themselves, so {@link containsDirectionalNode} probes their children instead.
1730
1674
  */
1731
1675
  const CONTAINER_TYPES = /* @__PURE__ */ new Set([
1732
- ast.schemaTypes.object,
1733
- ast.schemaTypes.array,
1734
- ast.schemaTypes.tuple,
1735
- ast.schemaTypes.union,
1736
- ast.schemaTypes.intersection,
1737
- ast.schemaTypes.ref
1676
+ ast.schema.types.object,
1677
+ ast.schema.types.array,
1678
+ ast.schema.types.tuple,
1679
+ ast.schema.types.union,
1680
+ ast.schema.types.intersection,
1681
+ ast.schema.types.ref
1738
1682
  ]);
1739
1683
  /**
1740
1684
  * Runs the node's effective handler (a `printer.nodes` override, else the built-in) once per
@@ -1765,7 +1709,7 @@ function variesByDirection({ node, printerOptions }) {
1765
1709
  */
1766
1710
  function containsDirectionalNode({ node, printerOptions }) {
1767
1711
  if (!node) return false;
1768
- return ast.findSchema(node, {
1712
+ return ast.schema.find(node, {
1769
1713
  predicate: (schema) => variesByDirection({
1770
1714
  node: schema,
1771
1715
  printerOptions
@@ -1777,10 +1721,10 @@ function containsDirectionalNode({ node, printerOptions }) {
1777
1721
  * Names of the `$ref` schemas the generator should route to their input (encode) variant.
1778
1722
  */
1779
1723
  function collectDirectionalRefNames({ node, printerOptions }) {
1780
- return ast.collectSync(node, { schema: (n) => n.type === ast.schemaTypes.ref && n.ref && containsDirectionalNode({
1724
+ return ast.node.collect(node, { schema: (n) => n.type === ast.schema.types.ref && n.ref && containsDirectionalNode({
1781
1725
  node: n,
1782
1726
  printerOptions
1783
- }) ? ast.resolveRefName(n) ?? void 0 : void 0 });
1727
+ }) ? ast.ref.getName(n) ?? void 0 : void 0 });
1784
1728
  }
1785
1729
  /**
1786
1730
  * Handlers that never recurse into children, so {@link variesByDirection} can call one directly
@@ -1802,6 +1746,7 @@ const standardDialect = {
1802
1746
  coercedNumber: (schema) => `z.coerce.number().pipe(${schema})`,
1803
1747
  catchall: (object, value) => `${object}.catchall(${value})`,
1804
1748
  nullable: (value) => `${value}.nullable()`,
1749
+ refine: (value, check) => `${value}.refine(${check})`,
1805
1750
  unique: (array) => `${array}.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })`,
1806
1751
  extend: (base, shape) => `${base}.extend(${shape})`,
1807
1752
  intersection: (left, right) => `${left}.and(${right})`,
@@ -1813,8 +1758,8 @@ const standardDialect = {
1813
1758
  };
1814
1759
  const scalarNodes = {
1815
1760
  ...buildScalarNodes(standardDialect),
1816
- bigint() {
1817
- return "z.coerce.bigint()";
1761
+ bigint(node) {
1762
+ return `z.coerce.bigint()${numberConstraints(bigintConstraints(node), "n")}`;
1818
1763
  },
1819
1764
  date(node) {
1820
1765
  if (node.representation !== "date") return "z.iso.date()";
@@ -1847,14 +1792,14 @@ const scalarNodes = {
1847
1792
  * const code = printer.print(stringNode) // "z.string()"
1848
1793
  * ```
1849
1794
  */
1850
- const printerZod = ast.createPrinter((options) => {
1795
+ const printerZod = createPrinter((options) => {
1851
1796
  return {
1852
1797
  name: "zod",
1853
1798
  options,
1854
- nodes: {
1799
+ nodes: withInputChecks({
1855
1800
  ...buildZodNodes(standardDialect, options.cyclicSchemas),
1856
1801
  ...scalarNodes
1857
- },
1802
+ }),
1858
1803
  overrides: options.nodes,
1859
1804
  print(node) {
1860
1805
  return printWithOmit(this, node, (value, meta) => applyModifiers({
@@ -1911,18 +1856,19 @@ const miniDialect = {
1911
1856
  coercedNumber: (schema) => `z.pipe(z.coerce.number(), ${schema})`,
1912
1857
  catchall: (object, value) => `z.catchall(${object}, ${value})`,
1913
1858
  nullable: (value) => `z.nullable(${value})`,
1859
+ refine: (value, check) => `${value}.check(z.refine(${check}))`,
1914
1860
  unique: (array) => `${array}.check(z.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" }))`,
1915
1861
  extend: (base, shape) => `z.extend(${base}, ${shape})`,
1916
1862
  intersection: (left, right) => `z.intersection(${left}, ${right})`
1917
1863
  };
1918
- const printerZodMini = ast.createPrinter((options) => {
1864
+ const printerZodMini = createPrinter((options) => {
1919
1865
  return {
1920
1866
  name: "zod-mini",
1921
1867
  options,
1922
- nodes: {
1868
+ nodes: withInputChecks({
1923
1869
  ...buildZodNodes(miniDialect, options.cyclicSchemas),
1924
1870
  bigint(node) {
1925
- return `z.coerce.bigint()${numberChecksMini(node)}`;
1871
+ return `z.coerce.bigint()${numberChecksMini(bigintConstraints(node), "n")}`;
1926
1872
  },
1927
1873
  date(node) {
1928
1874
  if (node.representation === "string") return "z.iso.date()";
@@ -1935,7 +1881,7 @@ const printerZodMini = ast.createPrinter((options) => {
1935
1881
  if (node.representation === "string") return "z.iso.time()";
1936
1882
  return "z.date()";
1937
1883
  }
1938
- },
1884
+ }),
1939
1885
  overrides: options.nodes,
1940
1886
  print(node) {
1941
1887
  return printWithOmit(this, node, (value, meta) => applyMiniModifiers({
@@ -2022,7 +1968,8 @@ const zodGenerator = defineGenerator({
2022
1968
  node,
2023
1969
  root,
2024
1970
  output,
2025
- group: group ?? void 0
1971
+ group: group ?? void 0,
1972
+ checks: true
2026
1973
  });
2027
1974
  const inputImportEntries = hasDirectionalNode ? [...directionalRefNames].map((schemaName) => ({
2028
1975
  name: [resolver.schema.inputName(schemaName)],
@@ -2113,7 +2060,7 @@ const zodGenerator = defineGenerator({
2113
2060
  });
2114
2061
  },
2115
2062
  operation(node, ctx) {
2116
- if (!ast.isHttpOperationNode(node)) return null;
2063
+ if (!ast.http.isOperation(node)) return null;
2117
2064
  const { adapter, config, resolver, root } = ctx;
2118
2065
  const { output, coercion, guidType, regexType, oneOfType, compile, mini, inferred, importPath, group, printer } = ctx.options;
2119
2066
  const dateType = getOasAdapter(adapter).options.dateType;
@@ -2142,17 +2089,18 @@ const zodGenerator = defineGenerator({
2142
2089
  node: schema,
2143
2090
  printerOptions
2144
2091
  })) : null;
2092
+ const schemaPrinter = getPrinter(resolver, {
2093
+ ...printerOptions,
2094
+ keysToOmit
2095
+ }, mini, direction);
2145
2096
  const imports = resolver.imports({
2146
2097
  node: schema,
2147
2098
  root,
2148
2099
  output,
2149
2100
  group: group ?? void 0,
2101
+ checks: true,
2150
2102
  name: (schemaName) => directionalRefNames?.has(schemaName) ? resolver.schema.inputName(schemaName) : resolver.name(schemaName)
2151
2103
  });
2152
- const schemaPrinter = getPrinter(resolver, {
2153
- ...printerOptions,
2154
- keysToOmit
2155
- }, mini, direction);
2156
2104
  return /* @__PURE__ */ jsxs(Fragment, { children: [imports.map((imp) => /* @__PURE__ */ jsx(File.Import, {
2157
2105
  root: meta.file.path,
2158
2106
  path: imp.path,
@@ -2192,7 +2140,7 @@ const zodGenerator = defineGenerator({
2192
2140
  })] });
2193
2141
  }
2194
2142
  function buildResponseUnion({ responses, name, fallbackUnknown }) {
2195
- if (new Set(responses.flatMap((res) => (res.content ?? []).flatMap((entry) => entry.schema ? ast.collectImportedRefNames(entry.schema).map((refName) => resolver.name(refName)) : []))).has(name)) return null;
2143
+ if (new Set(ast.http.responses.getImportedNames(responses).map((refName) => resolver.name(refName))).has(name)) return null;
2196
2144
  const members = responses.map((res) => ast.factory.createSchema({
2197
2145
  type: "ref",
2198
2146
  name: resolver.response.status(node, res.statusCode)
@@ -2225,14 +2173,27 @@ const zodGenerator = defineGenerator({
2225
2173
  keysToOmit: primary?.keysToOmit
2226
2174
  });
2227
2175
  });
2176
+ const toParamsSchema = (params) => ast.factory.createSchema({
2177
+ type: "object",
2178
+ primitive: "object",
2179
+ properties: params.map((param) => ast.factory.createProperty({
2180
+ name: param.name,
2181
+ required: param.required,
2182
+ schema: param.schema
2183
+ }))
2184
+ });
2185
+ const responseHeaderSchemas = node.responses.map((res) => res.headers?.length ? renderSchemaEntry({
2186
+ schema: toParamsSchema(res.headers),
2187
+ name: resolver.response.headers(node, res.statusCode)
2188
+ }) : null);
2228
2189
  const responsesWithSchema = node.responses.filter((res) => res.content?.some((entry) => entry.schema));
2229
- const successResponsesWithSchema = getSuccessResponses(responsesWithSchema);
2190
+ const successResponsesWithSchema = responsesWithSchema.filter((res) => ast.http.status.isSuccess(res));
2230
2191
  const responseUnionSchema = responsesWithSchema.length > 0 ? buildResponseUnion({
2231
2192
  responses: successResponsesWithSchema,
2232
2193
  name: resolver.response.response(node),
2233
2194
  fallbackUnknown: true
2234
2195
  }) : null;
2235
- const errorResponsesWithSchema = responsesWithSchema.filter((res) => !isSuccessStatusCode(res.statusCode));
2196
+ const errorResponsesWithSchema = responsesWithSchema.filter((res) => !ast.http.status.isSuccess(res));
2236
2197
  const errorUnionSchema = errorResponsesWithSchema.length > 0 ? buildResponseUnion({
2237
2198
  responses: errorResponsesWithSchema,
2238
2199
  name: resolver.response.error(node),
@@ -2259,7 +2220,7 @@ const zodGenerator = defineGenerator({
2259
2220
  description: node.requestBody.description ?? schema.description
2260
2221
  }), "encode");
2261
2222
  })();
2262
- const { path, query, header, cookie } = getOperationParameters(node);
2223
+ const { path, query, header, cookie } = ast.http.groupParameters(node);
2263
2224
  const paramGroupSchemas = [
2264
2225
  {
2265
2226
  kind: "path",
@@ -2278,15 +2239,7 @@ const zodGenerator = defineGenerator({
2278
2239
  params: cookie
2279
2240
  }
2280
2241
  ].filter(({ params }) => params.length > 0).map(({ kind, params }) => renderSchemaEntry({
2281
- schema: ast.factory.createSchema({
2282
- type: "object",
2283
- primitive: "object",
2284
- properties: params.map((param) => ast.factory.createProperty({
2285
- name: param.name,
2286
- required: param.required,
2287
- schema: param.schema
2288
- }))
2289
- }),
2242
+ schema: toParamsSchema(params),
2290
2243
  name: resolver.param[kind](node, params[0]),
2291
2244
  direction: "encode"
2292
2245
  }));
@@ -2327,6 +2280,7 @@ const zodGenerator = defineGenerator({
2327
2280
  }),
2328
2281
  paramSchemas,
2329
2282
  responseSchemas,
2283
+ responseHeaderSchemas,
2330
2284
  responseUnionSchema,
2331
2285
  errorUnionSchema,
2332
2286
  requestSchema,
@@ -2339,11 +2293,11 @@ const zodGenerator = defineGenerator({
2339
2293
  });
2340
2294
  // kubb-extension.json
2341
2295
  var compatibility = {
2342
- "kubb": ">=5.16.0",
2296
+ "kubb": ">=5.18.4",
2343
2297
  "node": ">=22.22"
2344
2298
  };
2345
2299
  // package.json
2346
- var version = "5.9.0-canary.20261111T122006";
2300
+ var version = "5.9.0";
2347
2301
  // src/resolvers/resolverZod.ts
2348
2302
  /**
2349
2303
  * Default resolver used by `@kubb/plugin-zod`. Decides the names and file
@@ -2363,15 +2317,15 @@ var version = "5.9.0-canary.20261111T122006";
2363
2317
  const resolverZod = createResolver({
2364
2318
  pluginName: "plugin-zod",
2365
2319
  name(name) {
2366
- return this.memo(`name:${name}`, () => ensureValidVarName(camelCase(name, { suffix: "schema" })));
2320
+ return this.memo(`name:${name}`, () => ensureValidVarName(casing.camel(name, { suffix: "schema" })));
2367
2321
  },
2368
- file: createCasedFile((part) => camelCase(part, { suffix: "schema" })),
2322
+ file: createCasedFile((part) => casing.camel(part, { suffix: "schema" })),
2369
2323
  schema: {
2370
2324
  typeName(name) {
2371
- return this.memo(`typeName:${name}`, () => ensureValidVarName(pascalCase(name, { suffix: "schema type" })));
2325
+ return this.memo(`typeName:${name}`, () => ensureValidVarName(casing.pascal(name, { suffix: "schema type" })));
2372
2326
  },
2373
2327
  type(name) {
2374
- return this.memo(`type:${name}`, () => ensureValidVarName(pascalCase(name, { suffix: "type" })));
2328
+ return this.memo(`type:${name}`, () => ensureValidVarName(casing.pascal(name, { suffix: "type" })));
2375
2329
  },
2376
2330
  inputName(name) {
2377
2331
  return this.name(`${name} input`);
@@ -2380,10 +2334,10 @@ const resolverZod = createResolver({
2380
2334
  return this.schema.typeName(`${name} input`);
2381
2335
  },
2382
2336
  isName(name) {
2383
- return ensureValidVarName(camelCase(name, { prefix: "is" }));
2337
+ return ensureValidVarName(casing.camel(name, { prefix: "is" }));
2384
2338
  },
2385
2339
  assertName(name) {
2386
- return ensureValidVarName(camelCase(name, { prefix: "assert" }));
2340
+ return ensureValidVarName(casing.camel(name, { prefix: "assert" }));
2387
2341
  }
2388
2342
  },
2389
2343
  param: createOperationParamResolver(),
@@ -2457,7 +2411,7 @@ const pluginZod = defineExtension({
2457
2411
  exclude,
2458
2412
  include,
2459
2413
  override,
2460
- group: createGroupConfig(group),
2414
+ group: group ?? null,
2461
2415
  importPath,
2462
2416
  coercion,
2463
2417
  inferred: inferred === "direction" ? "codec" : inferred,