@kubb/plugin-zod 5.0.0-beta.3 → 5.0.0-beta.30

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.
@@ -45,7 +45,7 @@ export type PrinterZodMiniOptions = {
45
45
  /**
46
46
  * Properties to exclude using `.omit({ key: true })`.
47
47
  */
48
- keysToOmit?: Array<string>
48
+ keysToOmit?: Array<string> | null
49
49
  /**
50
50
  * Schema names that form circular dependency chains.
51
51
  * Properties referencing these emit lazy getters wrapping refs in `z.lazy(() => …)`.
@@ -61,6 +61,22 @@ export type PrinterZodMiniOptions = {
61
61
  * Factory options for the Zod Mini printer, defining input/output types and configuration.
62
62
  */
63
63
  export type PrinterZodMiniFactory = ast.PrinterFactoryOptions<'zod-mini', PrinterZodMiniOptions, string, string>
64
+
65
+ function strictOneOfMember(member: string, node: ast.SchemaNode): string {
66
+ if (node.type === 'object' && (node.additionalProperties === undefined || node.additionalProperties === false)) {
67
+ return member.replace(/^z\.object\(/, 'z.strictObject(')
68
+ }
69
+
70
+ return member
71
+ }
72
+
73
+ function getMemberConstraintMini(member: ast.SchemaNode): string | undefined {
74
+ if (member.primitive === 'string') return lengthChecksMini(ast.narrowSchema(member, 'string') ?? {}) || undefined
75
+ if (member.primitive === 'number' || member.primitive === 'integer')
76
+ return numberChecksMini(ast.narrowSchema(member, 'number') ?? ast.narrowSchema(member, 'integer') ?? {}) || undefined
77
+ if (member.primitive === 'array') return lengthChecksMini(ast.narrowSchema(member, 'array') ?? {}) || undefined
78
+ }
79
+
64
80
  /**
65
81
  * Zod v4 **Mini** printer built with `definePrinter`.
66
82
  *
@@ -144,7 +160,7 @@ export const printerZodMini = ast.definePrinter<PrinterZodMiniFactory>((options)
144
160
  },
145
161
 
146
162
  ref(node) {
147
- if (!node.name) return undefined
163
+ if (!node.name) return null
148
164
  const refName = node.ref ? (ast.extractRefName(node.ref) ?? node.name) : node.name
149
165
  const resolvedName = node.ref ? (this.options.resolver?.default(refName, 'function') ?? refName) : node.name
150
166
 
@@ -168,9 +184,12 @@ export const printerZodMini = ast.definePrinter<PrinterZodMiniFactory>((options)
168
184
  const hasSelfRef = this.options.cyclicSchemas != null && ast.containsCircularRef(schema, { circularSchemas: this.options.cyclicSchemas })
169
185
  // Inside a getter the getter itself defers evaluation, so suppress
170
186
  // z.lazy() wrapping on nested refs by temporarily clearing cyclicSchemas.
187
+ // Save before clearing: this.options === options (same reference via definePrinter),
188
+ // so reading options.cyclicSchemas after mutation would return undefined.
189
+ const savedCyclicSchemas = this.options.cyclicSchemas
171
190
  if (hasSelfRef) this.options.cyclicSchemas = undefined
172
191
  const baseOutput = this.transform(schema) ?? this.transform(ast.createSchema({ type: 'unknown' }))!
173
- if (hasSelfRef) this.options.cyclicSchemas = options.cyclicSchemas
192
+ if (hasSelfRef) this.options.cyclicSchemas = savedCyclicSchemas
174
193
 
175
194
  const wrappedOutput = this.options.wrapOutput ? this.options.wrapOutput({ output: baseOutput, schema }) || baseOutput : baseOutput
176
195
 
@@ -194,13 +213,9 @@ export const printerZodMini = ast.definePrinter<PrinterZodMiniFactory>((options)
194
213
  array(node) {
195
214
  const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
196
215
  const inner = items.join(', ') || this.transform(ast.createSchema({ type: 'unknown' }))!
197
- let result = `z.array(${inner})${lengthChecksMini(node)}`
198
-
199
- if (node.unique) {
200
- result += `.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })`
201
- }
216
+ const base = `z.array(${inner})${lengthChecksMini(node)}`
202
217
 
203
- return result
218
+ return node.unique ? `${base}.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })` : base
204
219
  },
205
220
  tuple(node) {
206
221
  const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
@@ -209,7 +224,13 @@ export const printerZodMini = ast.definePrinter<PrinterZodMiniFactory>((options)
209
224
  },
210
225
  union(node) {
211
226
  const nodeMembers = node.members ?? []
212
- const members = nodeMembers.map((m) => this.transform(m)).filter(Boolean)
227
+ const members = nodeMembers
228
+ .map((memberNode) => {
229
+ const member = this.transform(memberNode)
230
+
231
+ return member && node.strategy === 'one' ? strictOneOfMember(member, memberNode) : member
232
+ })
233
+ .filter(Boolean)
213
234
  if (members.length === 0) return ''
214
235
  if (members.length === 1) return members[0]!
215
236
  if (node.discriminatorPropertyName && !nodeMembers.some((m) => m.type === 'intersection')) {
@@ -227,61 +248,37 @@ export const printerZodMini = ast.definePrinter<PrinterZodMiniFactory>((options)
227
248
  const [first, ...rest] = members
228
249
  if (!first) return ''
229
250
 
230
- let base = this.transform(first)
231
- if (!base) return ''
251
+ const firstBase = this.transform(first)
252
+ if (!firstBase) return ''
232
253
 
233
- for (const member of rest) {
234
- if (member.primitive === 'string') {
235
- const s = ast.narrowSchema(member, 'string')
236
- const c = lengthChecksMini(s ?? {})
237
- if (c) {
238
- base += c
239
- continue
240
- }
241
- } else if (member.primitive === 'number' || member.primitive === 'integer') {
242
- const n = ast.narrowSchema(member, 'number') ?? ast.narrowSchema(member, 'integer')
243
- const c = numberChecksMini(n ?? {})
244
- if (c) {
245
- base += c
246
- continue
247
- }
248
- } else if (member.primitive === 'array') {
249
- const a = ast.narrowSchema(member, 'array')
250
- const c = lengthChecksMini(a ?? {})
251
- if (c) {
252
- base += c
253
- continue
254
- }
255
- }
254
+ return rest.reduce((acc, member) => {
255
+ const constraint = getMemberConstraintMini(member)
256
+ if (constraint) return acc + constraint
256
257
  const transformed = this.transform(member)
257
- if (transformed) base = `z.intersection(${base}, ${transformed})`
258
- }
259
-
260
- return base
258
+ return transformed ? `z.intersection(${acc}, ${transformed})` : acc
259
+ }, firstBase)
261
260
  },
262
261
  ...options.nodes,
263
262
  },
264
263
  print(node) {
265
264
  const { keysToOmit } = this.options
266
265
 
267
- let base = this.transform(node)
268
- if (!base) return null
266
+ const transformed = this.transform(node)
267
+ if (!transformed) return null
269
268
 
270
269
  const meta = ast.syncSchemaRef(node)
271
270
 
272
- if (keysToOmit?.length && meta.primitive === 'object' && !(meta.type === 'union' && meta.discriminatorPropertyName)) {
271
+ const base = (() => {
272
+ if (!keysToOmit?.length || meta.primitive !== 'object' || (meta.type === 'union' && meta.discriminatorPropertyName)) return transformed
273
273
  // Mirror printerTs `nonNullable: true`: when omitting keys, the resulting
274
274
  // schema is a new non-nullable object type — skip optional/nullable/nullish.
275
275
  // Discriminated unions (z.discriminatedUnion) do not support .omit(), so skip them.
276
276
 
277
277
  // If this is a lazy reference, apply omit inside the lazy function
278
- const lazyMatch = base.match(/^z\.lazy\(\(\)\s*=>\s*(.+)\)$/)
279
- if (lazyMatch) {
280
- base = `z.lazy(() => ${lazyMatch[1]}.omit({ ${keysToOmit.map((k: string) => `"${k}": true`).join(', ')} }))`
281
- } else {
282
- base = `${base}.omit({ ${keysToOmit.map((k: string) => `"${k}": true`).join(', ')} })`
283
- }
284
- }
278
+ const lazyMatch = transformed.match(/^z\.lazy\(\(\)\s*=>\s*(.+)\)$/)
279
+ if (lazyMatch) return `z.lazy(() => ${lazyMatch[1]}.omit({ ${keysToOmit.map((k: string) => `"${k}": true`).join(', ')} }))`
280
+ return `${transformed}.omit({ ${keysToOmit.map((k: string) => `"${k}": true`).join(', ')} })`
281
+ })()
285
282
 
286
283
  return applyMiniModifiers({
287
284
  value: base,
@@ -1,57 +1,63 @@
1
- import { camelCase, pascalCase } from '@internals/utils'
1
+ import { camelCase, ensureValidVarName, pascalCase } from '@internals/utils'
2
2
  import { defineResolver } from '@kubb/core'
3
3
  import type { PluginZod } from '../types.ts'
4
4
 
5
5
  /**
6
- * Naming convention resolver for Zod plugin.
6
+ * Default resolver used by `@kubb/plugin-zod`. Decides the names and file
7
+ * paths for every generated Zod schema. Schemas use camelCase with a
8
+ * `Schema` suffix (`listPetsSchema`); their inferred types use PascalCase.
7
9
  *
8
- * Provides default naming helpers using camelCase with a `Schema` suffix for schemas.
10
+ * @example Resolve schema and type names
11
+ * ```ts
12
+ * import { resolverZod } from '@kubb/plugin-zod'
9
13
  *
10
- * @example
11
- * `resolverZod.default('list pets', 'function') // 'listPetsSchema'`
14
+ * resolverZod.default('list pets', 'function') // 'listPetsSchema'
15
+ * resolverZod.resolveSchemaTypeName('pet') // 'PetSchema'
16
+ * ```
12
17
  */
13
- export const resolverZod = defineResolver<PluginZod>((ctx) => {
18
+ export const resolverZod = defineResolver<PluginZod>(() => {
14
19
  return {
15
20
  name: 'default',
16
21
  pluginName: 'plugin-zod',
17
22
  default(name, type) {
18
- return camelCase(name, { isFile: type === 'file', suffix: type ? 'schema' : undefined })
23
+ const resolved = camelCase(name, { isFile: type === 'file', suffix: type ? 'schema' : undefined })
24
+ return type === 'file' ? resolved : ensureValidVarName(resolved)
19
25
  },
20
26
  resolveSchemaName(name) {
21
- return camelCase(name, { suffix: 'schema' })
27
+ return ensureValidVarName(camelCase(name, { suffix: 'schema' }))
22
28
  },
23
29
  resolveSchemaTypeName(name) {
24
- return pascalCase(name, { suffix: 'schema' })
30
+ return ensureValidVarName(pascalCase(name, { suffix: 'schema' }))
25
31
  },
26
32
  resolveTypeName(name) {
27
- return pascalCase(name)
33
+ return ensureValidVarName(pascalCase(name))
28
34
  },
29
35
  resolvePathName(name, type) {
30
- return ctx.default(name, type)
36
+ return this.default(name, type)
31
37
  },
32
38
  resolveParamName(node, param) {
33
- return ctx.resolveSchemaName(`${node.operationId} ${param.in} ${param.name}`)
39
+ return this.resolveSchemaName(`${node.operationId} ${param.in} ${param.name}`)
34
40
  },
35
41
  resolveResponseStatusName(node, statusCode) {
36
- return ctx.resolveSchemaName(`${node.operationId} Status ${statusCode}`)
42
+ return this.resolveSchemaName(`${node.operationId} Status ${statusCode}`)
37
43
  },
38
44
  resolveDataName(node) {
39
- return ctx.resolveSchemaName(`${node.operationId} Data`)
45
+ return this.resolveSchemaName(`${node.operationId} Data`)
40
46
  },
41
47
  resolveResponsesName(node) {
42
- return ctx.resolveSchemaName(`${node.operationId} Responses`)
48
+ return this.resolveSchemaName(`${node.operationId} Responses`)
43
49
  },
44
50
  resolveResponseName(node) {
45
- return ctx.resolveSchemaName(`${node.operationId} Response`)
51
+ return this.resolveSchemaName(`${node.operationId} Response`)
46
52
  },
47
53
  resolvePathParamsName(node, param) {
48
- return ctx.resolveParamName(node, param)
54
+ return this.resolveParamName(node, param)
49
55
  },
50
56
  resolveQueryParamsName(node, param) {
51
- return ctx.resolveParamName(node, param)
57
+ return this.resolveParamName(node, param)
52
58
  },
53
59
  resolveHeaderParamsName(node, param) {
54
- return ctx.resolveParamName(node, param)
60
+ return this.resolveParamName(node, param)
55
61
  },
56
62
  }
57
63
  })
package/src/types.ts CHANGED
@@ -75,85 +75,106 @@ export type ResolverZod = Resolver &
75
75
 
76
76
  export type Options = {
77
77
  /**
78
- * @default 'zod'
78
+ * Where the generated Zod schemas are written and how they are exported.
79
+ *
80
+ * @default { path: 'zod', barrel: { type: 'named' } }
79
81
  */
80
82
  output?: Output
81
83
  /**
82
- * Group the Zod schemas based on the provided name.
84
+ * Split generated files into subfolders based on the operation's tag.
83
85
  */
84
86
  group?: Group
85
87
  /**
86
- * Tags, operations, or paths to exclude from generation.
88
+ * Skip operations matching at least one entry in the list.
87
89
  */
88
90
  exclude?: Array<Exclude>
89
91
  /**
90
- * Tags, operations, or paths to include in generation.
92
+ * Restrict generation to operations matching at least one entry in the list.
91
93
  */
92
94
  include?: Array<Include>
93
95
  /**
94
- * Override options for specific tags, operations, or paths.
96
+ * Apply a different options object to operations matching a pattern.
95
97
  */
96
98
  override?: Array<Override<ResolvedOptions>>
97
99
  /**
98
- * Import path for Zod package.
100
+ * Module specifier used in the `import { z } from '...'` statement.
101
+ * Use `'zod/mini'` for the tree-shakeable bundle.
99
102
  *
100
103
  * @default 'zod'
101
104
  */
102
105
  importPath?: 'zod' | 'zod/mini' | (string & {})
103
106
  /**
104
- * Add TypeScript type annotations to generated schemas.
107
+ * Tie each Zod schema to its TypeScript type from `@kubb/plugin-ts`. Requires
108
+ * `@kubb/plugin-ts` in the plugins list. TypeScript fails compilation when the
109
+ * schema drifts from the type.
105
110
  */
106
111
  typed?: boolean
107
112
  /**
108
- * Return schemas as inferred types using `z.infer`.
113
+ * Export a `z.infer<typeof schema>` type alias next to every generated schema.
114
+ * Lets the Zod schema act as the single source of truth.
109
115
  */
110
116
  inferred?: boolean
111
117
  /**
112
- * Apply coercion to string values or configure coercion per type.
118
+ * Wrap schemas in `z.coerce` so input is coerced before validation. Useful for
119
+ * form data and query params where everything arrives as a string.
120
+ * - `true` coerces strings, numbers, and dates.
121
+ * - Object form picks per-primitive coercion.
122
+ *
123
+ * @default false
124
+ * @see https://zod.dev/?id=coercion-for-primitives
113
125
  */
114
126
  coercion?: boolean | { dates?: boolean; strings?: boolean; numbers?: boolean }
115
127
  /**
116
- * Generate operation-level schemas (grouped by operationId).
128
+ * Emit an `operations.ts` file with request body, query/path params, and per-status
129
+ * response schemas grouped by operation.
117
130
  */
118
131
  operations?: boolean
119
132
  /**
120
- * Validator to use for UUID format: `uuid` or `guid`.
133
+ * Validator for `format: uuid` properties.
134
+ * - `'uuid'` — `z.uuid()`. Standard RFC 4122.
135
+ * - `'guid'` — `z.guid()`. Accepts Microsoft-style GUIDs.
121
136
  *
122
137
  * @default 'uuid'
123
138
  */
124
139
  guidType?: 'uuid' | 'guid'
125
140
  /**
126
- * Use Zod Mini's functional API for better tree-shaking.
141
+ * Switch to Zod Mini's functional API for better tree-shaking. Also defaults
142
+ * `importPath` to `'zod/mini'`.
127
143
  *
128
144
  * @default false
145
+ * @beta
129
146
  */
130
147
  mini?: boolean
131
148
  /**
132
- * Callback to wrap the generated schema output.
133
- *
134
- * Useful for adding metadata like `.openapi()` or extension helpers.
149
+ * Wrap the generated Zod schema string with extra calls. Receives the raw output
150
+ * and the originating `SchemaNode`. Useful for round-tripping OpenAPI metadata
151
+ * back into Zod (e.g. `.openapi(...)`).
135
152
  */
136
153
  wrapOutput?: (arg: { output: string; schema: ast.SchemaNode }) => string | undefined
137
154
  /**
138
- * Apply casing to parameter names.
155
+ * Rename properties inside path/query/header schemas. Body schemas are unaffected.
156
+ *
157
+ * @note Must match the value of `paramsCasing` on `@kubb/plugin-ts`.
139
158
  */
140
159
  paramsCasing?: 'camelcase'
141
160
  /**
142
- * Additional generators alongside the default generators.
161
+ * Custom generators that run alongside the built-in Zod generators.
143
162
  */
144
163
  generators?: Array<Generator<PluginZod>>
145
164
  /**
146
- * Override naming conventions for schema names and types.
165
+ * Override how schema and operation names are built. Methods you omit fall back
166
+ * to the default `resolverZod`.
147
167
  */
148
168
  resolver?: Partial<ResolverZod> & ThisType<ResolverZod>
149
169
  /**
150
- * Override printer node handlers to customize rendering of specific schema types.
170
+ * Replace the Zod handler for a specific schema type (`'integer'`, `'date'`, ...).
171
+ * When `mini: true`, overrides target the Zod Mini printer instead.
151
172
  */
152
173
  printer?: {
153
174
  nodes?: PrinterZodNodes | PrinterZodMiniNodes
154
175
  }
155
176
  /**
156
- * AST visitor to transform schema and operation nodes.
177
+ * AST visitor applied to each schema or operation node before printing.
157
178
  */
158
179
  transformer?: ast.Visitor
159
180
  }
@@ -163,7 +184,7 @@ type ResolvedOptions = {
163
184
  exclude: Array<Exclude>
164
185
  include: Array<Include> | undefined
165
186
  override: Array<Override<ResolvedOptions>>
166
- group: Group | undefined
187
+ group: Group | null
167
188
  typed: NonNullable<Options['typed']>
168
189
  inferred: NonNullable<Options['inferred']>
169
190
  importPath: NonNullable<Options['importPath']>
package/src/utils.ts CHANGED
@@ -39,11 +39,11 @@ export function buildSchemaNames(node: ast.OperationNode, { params, resolver }:
39
39
  responses['default'] = resolver.resolveResponseName(node)
40
40
 
41
41
  return {
42
- request: node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : undefined,
42
+ request: node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : null,
43
43
  parameters: {
44
- path: pathParam ? resolver.resolvePathParamsName(node, pathParam) : undefined,
45
- query: queryParam ? resolver.resolveQueryParamsName(node, queryParam) : undefined,
46
- header: headerParam ? resolver.resolveHeaderParamsName(node, headerParam) : undefined,
44
+ path: pathParam ? resolver.resolvePathParamsName(node, pathParam) : null,
45
+ query: queryParam ? resolver.resolveQueryParamsName(node, queryParam) : null,
46
+ header: headerParam ? resolver.resolveHeaderParamsName(node, headerParam) : null,
47
47
  },
48
48
  responses,
49
49
  errors,
@@ -133,7 +133,7 @@ export function lengthConstraints({ min, max, pattern }: LengthConstraints): str
133
133
  * Build `.check(z.minimum(), z.maximum())` for `zod/mini` numeric constraints.
134
134
  */
135
135
  export function numberChecksMini({ min, max, exclusiveMinimum, exclusiveMaximum, multipleOf }: NumericConstraints): string {
136
- const checks: string[] = []
136
+ const checks: Array<string> = []
137
137
  if (min !== undefined) checks.push(`z.minimum(${min})`)
138
138
  if (max !== undefined) checks.push(`z.maximum(${max})`)
139
139
  if (exclusiveMinimum !== undefined) checks.push(`z.minimum(${exclusiveMinimum}, { exclusive: true })`)
@@ -146,7 +146,7 @@ export function numberChecksMini({ min, max, exclusiveMinimum, exclusiveMaximum,
146
146
  * Build `.check(z.minLength(), z.maxLength(), z.regex())` for `zod/mini` length constraints.
147
147
  */
148
148
  export function lengthChecksMini({ min, max, pattern }: LengthConstraints): string {
149
- const checks: string[] = []
149
+ const checks: Array<string> = []
150
150
  if (min !== undefined) checks.push(`z.minLength(${min})`)
151
151
  if (max !== undefined) checks.push(`z.maxLength(${max})`)
152
152
  if (pattern !== undefined) checks.push(`z.regex(${toRegExpString(pattern, null)})`)
@@ -158,21 +158,14 @@ export function lengthChecksMini({ min, max, pattern }: LengthConstraints): stri
158
158
  * to a schema value string using the chainable Zod v4 API.
159
159
  */
160
160
  export function applyModifiers({ value, nullable, optional, nullish, defaultValue, description }: ModifierOptions): string {
161
- let result = value
162
- if (nullish || (nullable && optional)) {
163
- result = `${result}.nullish()`
164
- } else if (optional) {
165
- result = `${result}.optional()`
166
- } else if (nullable) {
167
- result = `${result}.nullable()`
168
- }
169
- if (defaultValue !== undefined) {
170
- result = `${result}.default(${formatDefault(defaultValue)})`
171
- }
172
- if (description) {
173
- result = `${result}.describe(${stringify(description)})`
174
- }
175
- return result
161
+ const withModifier = (() => {
162
+ if (nullish || (nullable && optional)) return `${value}.nullish()`
163
+ if (optional) return `${value}.optional()`
164
+ if (nullable) return `${value}.nullable()`
165
+ return value
166
+ })()
167
+ const withDefault = defaultValue !== undefined ? `${withModifier}.default(${formatDefault(defaultValue)})` : withModifier
168
+ return description ? `${withDefault}.describe(${stringify(description)})` : withDefault
176
169
  }
177
170
 
178
171
  /**
@@ -180,21 +173,12 @@ export function applyModifiers({ value, nullable, optional, nullish, defaultValu
180
173
  * (`z.nullable()`, `z.optional()`, `z.nullish()`).
181
174
  */
182
175
  export function applyMiniModifiers({ value, nullable, optional, nullish, defaultValue }: Omit<ModifierOptions, 'description'>): string {
183
- let result = value
184
- if (nullish) {
185
- result = `z.nullish(${result})`
186
- } else {
187
- if (nullable) {
188
- result = `z.nullable(${result})`
189
- }
190
- if (optional) {
191
- result = `z.optional(${result})`
192
- }
193
- }
194
- if (defaultValue !== undefined) {
195
- result = `z._default(${result}, ${formatDefault(defaultValue)})`
196
- }
197
- return result
176
+ const withModifier = (() => {
177
+ if (nullish) return `z.nullish(${value})`
178
+ const withNullable = nullable ? `z.nullable(${value})` : value
179
+ return optional ? `z.optional(${withNullable})` : withNullable
180
+ })()
181
+ return defaultValue !== undefined ? `z._default(${withModifier}, ${formatDefault(defaultValue)})` : withModifier
198
182
  }
199
183
 
200
184
  type BuildGroupedParamsSchemaOptions = {