@kubb/plugin-zod 5.0.0-beta.15 → 5.0.0-beta.25

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubb/plugin-zod",
3
- "version": "5.0.0-beta.15",
3
+ "version": "5.0.0-beta.25",
4
4
  "description": "Generate Zod validation schemas from your OpenAPI specification for runtime data parsing and type safety. Pairs perfectly with @kubb/plugin-ts for end-to-end type coverage.",
5
5
  "keywords": [
6
6
  "code-generation",
@@ -48,15 +48,15 @@
48
48
  "registry": "https://registry.npmjs.org/"
49
49
  },
50
50
  "dependencies": {
51
- "@kubb/core": "5.0.0-beta.19",
52
- "@kubb/renderer-jsx": "5.0.0-beta.19",
51
+ "@kubb/core": "5.0.0-beta.25",
52
+ "@kubb/renderer-jsx": "5.0.0-beta.25",
53
53
  "remeda": "^2.34.1"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@internals/utils": "0.0.0"
57
57
  },
58
58
  "peerDependencies": {
59
- "@kubb/renderer-jsx": "5.0.0-beta.19"
59
+ "@kubb/renderer-jsx": "5.0.0-beta.25"
60
60
  },
61
61
  "size-limit": [
62
62
  {
@@ -4,11 +4,11 @@ import { Const, File, Type } from '@kubb/renderer-jsx'
4
4
  import type { KubbReactNode } from '@kubb/renderer-jsx/types'
5
5
 
6
6
  type SchemaNames = {
7
- request: string | undefined
7
+ request: string | null
8
8
  parameters: {
9
- path: string | undefined
10
- query: string | undefined
11
- header: string | undefined
9
+ path: string | null
10
+ query: string | null
11
+ header: string | null
12
12
  }
13
13
  responses: { default?: string } & Record<number | string, string>
14
14
  errors: Record<number | string, string>
@@ -13,7 +13,7 @@ type Props = {
13
13
  * then merges in any user-supplied `printer.nodes` overrides.
14
14
  */
15
15
  printer: ast.Printer<PrinterZodFactory> | ast.Printer<PrinterZodMiniFactory>
16
- inferTypeName?: string
16
+ inferTypeName?: string | null
17
17
  }
18
18
 
19
19
  export function Zod({ name, node, printer, inferTypeName }: Props): KubbReactNode {
@@ -17,11 +17,17 @@ type ZodMiniPrinterEntry = { printer: ReturnType<typeof printerZodMini>; guidTyp
17
17
  const zodPrinterCache = new WeakMap<ResolverZod, ZodPrinterEntry>()
18
18
  const zodMiniPrinterCache = new WeakMap<ResolverZod, ZodMiniPrinterEntry>()
19
19
 
20
+ /**
21
+ * Built-in generator for `@kubb/plugin-zod`. Emits one Zod schema per
22
+ * schema in the spec plus per-operation request/response/parameter schemas.
23
+ * When `mini: true`, schemas use the Zod Mini functional API instead of
24
+ * chainable methods.
25
+ */
20
26
  export const zodGenerator = defineGenerator<PluginZod>({
21
27
  name: 'zod',
22
28
  renderer: jsxRendererSync,
23
29
  schema(node, ctx) {
24
- const { adapter, config, resolver, root, inputNode } = ctx
30
+ const { adapter, config, resolver, root } = ctx
25
31
  const { output, coercion, guidType, mini, wrapOutput, inferred, importPath, group, printer } = ctx.options
26
32
  const dateType = (adapter as Adapter<AdapterOas>).options.dateType
27
33
 
@@ -34,17 +40,17 @@ export const zodGenerator = defineGenerator<PluginZod>({
34
40
 
35
41
  const imports = adapter.getImports(node, (schemaName) => ({
36
42
  name: resolver.resolveSchemaName(schemaName),
37
- path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }).path,
43
+ path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group: group ?? undefined }).path,
38
44
  }))
39
45
 
40
46
  const meta = {
41
47
  name: resolver.resolveSchemaName(node.name),
42
- file: resolver.resolveFile({ name: node.name, extname: '.ts' }, { root, output, group }),
48
+ file: resolver.resolveFile({ name: node.name, extname: '.ts' }, { root, output, group: group ?? undefined }),
43
49
  } as const
44
50
 
45
- const inferTypeName = inferred ? resolver.resolveSchemaTypeName(node.name) : undefined
51
+ const inferTypeName = inferred ? resolver.resolveSchemaTypeName(node.name) : null
46
52
 
47
- const cyclicSchemas = ast.findCircularSchemas(inputNode.schemas)
53
+ const cyclicSchemas = new Set<string>(ctx.meta.circularNames)
48
54
 
49
55
  const schemaPrinter = mini ? getCachedMiniPrinter() : getCachedStdPrinter()
50
56
  function getCachedStdPrinter() {
@@ -71,8 +77,8 @@ export const zodGenerator = defineGenerator<PluginZod>({
71
77
  baseName={meta.file.baseName}
72
78
  path={meta.file.path}
73
79
  meta={meta.file.meta}
74
- banner={resolver.resolveBanner(inputNode, { output, config })}
75
- footer={resolver.resolveFooter(inputNode, { output, config })}
80
+ banner={resolver.resolveBanner(ctx.meta, { output, config })}
81
+ footer={resolver.resolveFooter(ctx.meta, { output, config })}
76
82
  >
77
83
  <File.Import name={isZodImport ? 'z' : ['z']} path={importPath} isNameSpace={isZodImport} />
78
84
  {mode === 'split' && imports.map((imp) => <File.Import key={[node.name, imp.path].join('-')} root={meta.file.path} path={imp.path} name={imp.name} />)}
@@ -82,7 +88,7 @@ export const zodGenerator = defineGenerator<PluginZod>({
82
88
  )
83
89
  },
84
90
  operation(node, ctx) {
85
- const { adapter, config, resolver, root, inputNode } = ctx
91
+ const { adapter, config, resolver, root } = ctx
86
92
  const { output, coercion, guidType, mini, wrapOutput, inferred, importPath, group, paramsCasing, printer } = ctx.options
87
93
  const dateType = (adapter as Adapter<AdapterOas>).options.dateType
88
94
 
@@ -92,19 +98,22 @@ export const zodGenerator = defineGenerator<PluginZod>({
92
98
  const params = ast.caseParams(node.parameters, paramsCasing)
93
99
 
94
100
  const meta = {
95
- file: resolver.resolveFile({ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path }, { root, output, group }),
101
+ file: resolver.resolveFile(
102
+ { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
103
+ { root, output, group: group ?? undefined },
104
+ ),
96
105
  } as const
97
106
 
98
- const cyclicSchemas = ast.findCircularSchemas(inputNode.schemas)
107
+ const cyclicSchemas = new Set<string>(ctx.meta.circularNames)
99
108
 
100
- function renderSchemaEntry({ schema, name, keysToOmit }: { schema: ast.SchemaNode | null; name: string; keysToOmit?: Array<string> }) {
109
+ function renderSchemaEntry({ schema, name, keysToOmit }: { schema: ast.SchemaNode | null; name: string; keysToOmit?: Array<string> | null }) {
101
110
  if (!schema) return null
102
111
 
103
- const inferTypeName = inferred ? resolver.resolveTypeName(name) : undefined
112
+ const inferTypeName = inferred ? resolver.resolveTypeName(name) : null
104
113
 
105
114
  const imports = adapter.getImports(schema, (schemaName) => ({
106
115
  name: resolver.resolveSchemaName(schemaName),
107
- path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }).path,
116
+ path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group: group ?? undefined }).path,
108
117
  }))
109
118
 
110
119
  const cachedStd = zodPrinterCache.get(resolver)
@@ -192,8 +201,8 @@ export const zodGenerator = defineGenerator<PluginZod>({
192
201
  baseName={meta.file.baseName}
193
202
  path={meta.file.path}
194
203
  meta={meta.file.meta}
195
- banner={resolver.resolveBanner(inputNode, { output, config })}
196
- footer={resolver.resolveFooter(inputNode, { output, config })}
204
+ banner={resolver.resolveBanner(ctx.meta, { output, config })}
205
+ footer={resolver.resolveFooter(ctx.meta, { output, config })}
197
206
  >
198
207
  <File.Import name={isZodImport ? 'z' : ['z']} path={importPath} isNameSpace={isZodImport} />
199
208
  {paramSchemas}
@@ -204,7 +213,7 @@ export const zodGenerator = defineGenerator<PluginZod>({
204
213
  )
205
214
  },
206
215
  operations(nodes, ctx) {
207
- const { config, resolver, root, inputNode } = ctx
216
+ const { config, resolver, root } = ctx
208
217
  const { output, importPath, group, operations, paramsCasing } = ctx.options
209
218
 
210
219
  if (!operations) {
@@ -213,7 +222,7 @@ export const zodGenerator = defineGenerator<PluginZod>({
213
222
  const isZodImport = ZOD_NAMESPACE_IMPORTS.has(importPath as 'zod' | 'zod/mini')
214
223
 
215
224
  const meta = {
216
- file: resolver.resolveFile({ name: 'operations', extname: '.ts' }, { root, output, group }),
225
+ file: resolver.resolveFile({ name: 'operations', extname: '.ts' }, { root, output, group: group ?? undefined }),
217
226
  } as const
218
227
 
219
228
  const transformedOperations = nodes.map((node) => {
@@ -226,8 +235,11 @@ export const zodGenerator = defineGenerator<PluginZod>({
226
235
  })
227
236
 
228
237
  const imports = transformedOperations.flatMap(({ node, data }) => {
229
- const names = [data.request, ...Object.values(data.responses), ...Object.values(data.parameters)].filter(Boolean) as string[]
230
- const opFile = resolver.resolveFile({ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path }, { root, output, group })
238
+ const names = [data.request, ...Object.values(data.responses), ...Object.values(data.parameters)].filter(Boolean) as Array<string>
239
+ const opFile = resolver.resolveFile(
240
+ { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
241
+ { root, output, group: group ?? undefined },
242
+ )
231
243
 
232
244
  return names.map((name) => <File.Import key={[name, opFile.path].join('-')} name={[name]} root={meta.file.path} path={opFile.path} />)
233
245
  })
@@ -237,8 +249,8 @@ export const zodGenerator = defineGenerator<PluginZod>({
237
249
  baseName={meta.file.baseName}
238
250
  path={meta.file.path}
239
251
  meta={meta.file.meta}
240
- banner={resolver.resolveBanner(inputNode, { output, config })}
241
- footer={resolver.resolveFooter(inputNode, { output, config })}
252
+ banner={resolver.resolveBanner(ctx.meta, { output, config })}
253
+ footer={resolver.resolveFooter(ctx.meta, { output, config })}
242
254
  >
243
255
  <File.Import isTypeOnly name={isZodImport ? 'z' : ['z']} path={importPath} isNameSpace={isZodImport} />
244
256
  {imports}
package/src/plugin.ts CHANGED
@@ -5,20 +5,33 @@ import { resolverZod } from './resolvers/resolverZod.ts'
5
5
  import type { PluginZod } from './types.ts'
6
6
 
7
7
  /**
8
- * Canonical plugin name for `@kubb/plugin-zod`, used in driver lookups and warnings.
8
+ * Canonical plugin name for `@kubb/plugin-zod`. Used for driver lookups and
9
+ * cross-plugin dependency references.
9
10
  */
10
11
  export const pluginZodName = 'plugin-zod' satisfies PluginZod['name']
11
12
 
12
13
  /**
13
- * Generates Zod validation schemas from an OpenAPI specification.
14
- * Walks schemas and operations, delegates to generators, and writes barrel files
15
- * based on the configured `barrelType`.
14
+ * Generates Zod v4 schemas from an OpenAPI spec. Use them to validate API
15
+ * responses at runtime, build form schemas, or feed back into router libraries
16
+ * that consume Zod (tRPC, Hono, Elysia). Pair with `@kubb/plugin-client` and
17
+ * set the client's `parser: 'zod'` to validate every response automatically.
16
18
  *
17
- * @example Zod schema generator
19
+ * @example
18
20
  * ```ts
19
- * import pluginZod from '@kubb/plugin-zod'
21
+ * import { defineConfig } from 'kubb'
22
+ * import { pluginTs } from '@kubb/plugin-ts'
23
+ * import { pluginZod } from '@kubb/plugin-zod'
24
+ *
20
25
  * export default defineConfig({
21
- * plugins: [pluginZod({ output: { path: 'zod' } })]
26
+ * input: { path: './petStore.yaml' },
27
+ * output: { path: './src/gen' },
28
+ * plugins: [
29
+ * pluginTs(),
30
+ * pluginZod({
31
+ * output: { path: './zod' },
32
+ * typed: true,
33
+ * }),
34
+ * ],
22
35
  * })
23
36
  * ```
24
37
  */
@@ -54,7 +67,7 @@ export const pluginZod = definePlugin<PluginZod>((options) => {
54
67
  return `${camelCase(ctx.group)}Controller`
55
68
  },
56
69
  } satisfies Group)
57
- : undefined
70
+ : null
58
71
 
59
72
  return {
60
73
  name: pluginZodName,
@@ -53,7 +53,7 @@ export type PrinterZodOptions = {
53
53
  /**
54
54
  * Properties to exclude using `.omit({ key: true })`.
55
55
  */
56
- keysToOmit?: Array<string>
56
+ keysToOmit?: Array<string> | null
57
57
  /**
58
58
  * Schema names that form circular dependency chains.
59
59
  * Properties referencing these emit lazy getters wrapping refs in `z.lazy(() => …)`.
@@ -90,6 +90,13 @@ function strictOneOfMember(member: string, node: ast.SchemaNode): string {
90
90
  return member
91
91
  }
92
92
 
93
+ function getMemberConstraint(member: ast.SchemaNode): string | undefined {
94
+ if (member.primitive === 'string') return lengthConstraints(ast.narrowSchema(member, 'string') ?? {}) || undefined
95
+ if (member.primitive === 'number' || member.primitive === 'integer')
96
+ return numberConstraints(ast.narrowSchema(member, 'number') ?? ast.narrowSchema(member, 'integer') ?? {}) || undefined
97
+ if (member.primitive === 'array') return lengthConstraints(ast.narrowSchema(member, 'array') ?? {}) || undefined
98
+ }
99
+
93
100
  /**
94
101
  * Zod v4 printer built with `definePrinter`.
95
102
  *
@@ -184,7 +191,7 @@ export const printerZod = ast.definePrinter<PrinterZodFactory>((options) => {
184
191
  return `z.enum([${nonNullValues.map(formatLiteral).join(', ')}])`
185
192
  },
186
193
  ref(node) {
187
- if (!node.name) return undefined
194
+ if (!node.name) return null
188
195
  const refName = node.ref ? (ast.extractRefName(node.ref) ?? node.name) : node.name
189
196
  const resolvedName = node.ref ? (this.options.resolver?.default(refName, 'function') ?? refName) : node.name
190
197
 
@@ -220,10 +227,7 @@ export const printerZod = ast.definePrinter<PrinterZodFactory>((options) => {
220
227
  // When a property schema is not a ref but the metadata is from a ref (e.g., discriminator
221
228
  // property override), skip applying the description from the ref target to avoid applying
222
229
  // metadata from a replaced schema.
223
- let descriptionToApply = meta.description
224
- if (schema.type !== 'ref' && meta.type === 'ref') {
225
- descriptionToApply = undefined
226
- }
230
+ const descriptionToApply = schema.type !== 'ref' && meta.type === 'ref' ? undefined : meta.description
227
231
 
228
232
  const value = applyModifiers({
229
233
  value: wrappedOutput,
@@ -241,32 +245,27 @@ export const printerZod = ast.definePrinter<PrinterZodFactory>((options) => {
241
245
  })
242
246
  .join(',\n ')
243
247
 
244
- let result = `z.object({\n ${properties}\n })`
248
+ const objectBase = `z.object({\n ${properties}\n })`
245
249
 
246
250
  // Handle additionalProperties as .catchall() or .strict()
247
- if (node.additionalProperties && node.additionalProperties !== true) {
248
- const catchallType = this.transform(node.additionalProperties)
249
- if (catchallType) {
250
- result += `.catchall(${catchallType})`
251
+ const result = (() => {
252
+ if (node.additionalProperties && node.additionalProperties !== true) {
253
+ const catchallType = this.transform(node.additionalProperties)
254
+ return catchallType ? `${objectBase}.catchall(${catchallType})` : objectBase
251
255
  }
252
- } else if (node.additionalProperties === true) {
253
- result += `.catchall(${this.transform(ast.createSchema({ type: 'unknown' }))})`
254
- } else if (node.additionalProperties === false) {
255
- result += '.strict()'
256
- }
256
+ if (node.additionalProperties === true) return `${objectBase}.catchall(${this.transform(ast.createSchema({ type: 'unknown' }))})`
257
+ if (node.additionalProperties === false) return `${objectBase}.strict()`
258
+ return objectBase
259
+ })()
257
260
 
258
261
  return result
259
262
  },
260
263
  array(node) {
261
264
  const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
262
265
  const inner = items.join(', ') || this.transform(ast.createSchema({ type: 'unknown' }))!
263
- let result = `z.array(${inner})${lengthConstraints(node)}`
266
+ const base = `z.array(${inner})${lengthConstraints(node)}`
264
267
 
265
- if (node.unique) {
266
- result += `.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })`
267
- }
268
-
269
- return result
268
+ return node.unique ? `${base}.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })` : base
270
269
  },
271
270
  tuple(node) {
272
271
  const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
@@ -299,61 +298,37 @@ export const printerZod = ast.definePrinter<PrinterZodFactory>((options) => {
299
298
  const [first, ...rest] = members
300
299
  if (!first) return ''
301
300
 
302
- let base = this.transform(first)
303
- if (!base) return ''
301
+ const firstBase = this.transform(first)
302
+ if (!firstBase) return ''
304
303
 
305
- for (const member of rest) {
306
- if (member.primitive === 'string') {
307
- const s = ast.narrowSchema(member, 'string')
308
- const c = lengthConstraints(s ?? {})
309
- if (c) {
310
- base += c
311
- continue
312
- }
313
- } else if (member.primitive === 'number' || member.primitive === 'integer') {
314
- const n = ast.narrowSchema(member, 'number') ?? ast.narrowSchema(member, 'integer')
315
- const c = numberConstraints(n ?? {})
316
- if (c) {
317
- base += c
318
- continue
319
- }
320
- } else if (member.primitive === 'array') {
321
- const a = ast.narrowSchema(member, 'array')
322
- const c = lengthConstraints(a ?? {})
323
- if (c) {
324
- base += c
325
- continue
326
- }
327
- }
304
+ return rest.reduce((acc, member) => {
305
+ const constraint = getMemberConstraint(member)
306
+ if (constraint) return acc + constraint
328
307
  const transformed = this.transform(member)
329
- if (transformed) base = `${base}.and(${transformed})`
330
- }
331
-
332
- return base
308
+ return transformed ? `${acc}.and(${transformed})` : acc
309
+ }, firstBase)
333
310
  },
334
311
  ...options.nodes,
335
312
  },
336
313
  print(node) {
337
314
  const { keysToOmit } = this.options
338
315
 
339
- let base = this.transform(node)
340
- if (!base) return null
316
+ const transformed = this.transform(node)
317
+ if (!transformed) return null
341
318
 
342
319
  const meta = ast.syncSchemaRef(node)
343
320
 
344
- if (keysToOmit?.length && meta.primitive === 'object' && !(meta.type === 'union' && meta.discriminatorPropertyName)) {
321
+ const base = (() => {
322
+ if (!keysToOmit?.length || meta.primitive !== 'object' || (meta.type === 'union' && meta.discriminatorPropertyName)) return transformed
345
323
  // Mirror printerTs `nonNullable: true`: when omitting keys, the resulting
346
324
  // schema is a new non-nullable object type — skip optional/nullable/nullish.
347
325
  // Discriminated unions (z.discriminatedUnion) do not support .omit(), so skip them.
348
326
 
349
327
  // If this is a lazy reference, apply omit inside the lazy function
350
- const lazyMatch = base.match(/^z\.lazy\(\(\)\s*=>\s*(.+)\)$/)
351
- if (lazyMatch) {
352
- base = `z.lazy(() => ${lazyMatch[1]}.omit({ ${keysToOmit.map((k: string) => `"${k}": true`).join(', ')} }))`
353
- } else {
354
- base = `${base}.omit({ ${keysToOmit.map((k: string) => `"${k}": true`).join(', ')} })`
355
- }
356
- }
328
+ const lazyMatch = transformed.match(/^z\.lazy\(\(\)\s*=>\s*(.+)\)$/)
329
+ if (lazyMatch) return `z.lazy(() => ${lazyMatch[1]}.omit({ ${keysToOmit.map((k: string) => `"${k}": true`).join(', ')} }))`
330
+ return `${transformed}.omit({ ${keysToOmit.map((k: string) => `"${k}": true`).join(', ')} })`
331
+ })()
357
332
 
358
333
  return applyModifiers({
359
334
  value: base,
@@ -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(() => …)`.
@@ -70,6 +70,13 @@ function strictOneOfMember(member: string, node: ast.SchemaNode): string {
70
70
  return member
71
71
  }
72
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
+
73
80
  /**
74
81
  * Zod v4 **Mini** printer built with `definePrinter`.
75
82
  *
@@ -153,7 +160,7 @@ export const printerZodMini = ast.definePrinter<PrinterZodMiniFactory>((options)
153
160
  },
154
161
 
155
162
  ref(node) {
156
- if (!node.name) return undefined
163
+ if (!node.name) return null
157
164
  const refName = node.ref ? (ast.extractRefName(node.ref) ?? node.name) : node.name
158
165
  const resolvedName = node.ref ? (this.options.resolver?.default(refName, 'function') ?? refName) : node.name
159
166
 
@@ -206,13 +213,9 @@ export const printerZodMini = ast.definePrinter<PrinterZodMiniFactory>((options)
206
213
  array(node) {
207
214
  const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
208
215
  const inner = items.join(', ') || this.transform(ast.createSchema({ type: 'unknown' }))!
209
- let result = `z.array(${inner})${lengthChecksMini(node)}`
216
+ const base = `z.array(${inner})${lengthChecksMini(node)}`
210
217
 
211
- if (node.unique) {
212
- result += `.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })`
213
- }
214
-
215
- return result
218
+ return node.unique ? `${base}.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })` : base
216
219
  },
217
220
  tuple(node) {
218
221
  const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
@@ -245,61 +248,37 @@ export const printerZodMini = ast.definePrinter<PrinterZodMiniFactory>((options)
245
248
  const [first, ...rest] = members
246
249
  if (!first) return ''
247
250
 
248
- let base = this.transform(first)
249
- if (!base) return ''
251
+ const firstBase = this.transform(first)
252
+ if (!firstBase) return ''
250
253
 
251
- for (const member of rest) {
252
- if (member.primitive === 'string') {
253
- const s = ast.narrowSchema(member, 'string')
254
- const c = lengthChecksMini(s ?? {})
255
- if (c) {
256
- base += c
257
- continue
258
- }
259
- } else if (member.primitive === 'number' || member.primitive === 'integer') {
260
- const n = ast.narrowSchema(member, 'number') ?? ast.narrowSchema(member, 'integer')
261
- const c = numberChecksMini(n ?? {})
262
- if (c) {
263
- base += c
264
- continue
265
- }
266
- } else if (member.primitive === 'array') {
267
- const a = ast.narrowSchema(member, 'array')
268
- const c = lengthChecksMini(a ?? {})
269
- if (c) {
270
- base += c
271
- continue
272
- }
273
- }
254
+ return rest.reduce((acc, member) => {
255
+ const constraint = getMemberConstraintMini(member)
256
+ if (constraint) return acc + constraint
274
257
  const transformed = this.transform(member)
275
- if (transformed) base = `z.intersection(${base}, ${transformed})`
276
- }
277
-
278
- return base
258
+ return transformed ? `z.intersection(${acc}, ${transformed})` : acc
259
+ }, firstBase)
279
260
  },
280
261
  ...options.nodes,
281
262
  },
282
263
  print(node) {
283
264
  const { keysToOmit } = this.options
284
265
 
285
- let base = this.transform(node)
286
- if (!base) return null
266
+ const transformed = this.transform(node)
267
+ if (!transformed) return null
287
268
 
288
269
  const meta = ast.syncSchemaRef(node)
289
270
 
290
- 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
291
273
  // Mirror printerTs `nonNullable: true`: when omitting keys, the resulting
292
274
  // schema is a new non-nullable object type — skip optional/nullable/nullish.
293
275
  // Discriminated unions (z.discriminatedUnion) do not support .omit(), so skip them.
294
276
 
295
277
  // If this is a lazy reference, apply omit inside the lazy function
296
- const lazyMatch = base.match(/^z\.lazy\(\(\)\s*=>\s*(.+)\)$/)
297
- if (lazyMatch) {
298
- base = `z.lazy(() => ${lazyMatch[1]}.omit({ ${keysToOmit.map((k: string) => `"${k}": true`).join(', ')} }))`
299
- } else {
300
- base = `${base}.omit({ ${keysToOmit.map((k: string) => `"${k}": true`).join(', ')} })`
301
- }
302
- }
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
+ })()
303
282
 
304
283
  return applyMiniModifiers({
305
284
  value: base,
@@ -3,12 +3,17 @@ 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
18
  export const resolverZod = defineResolver<PluginZod>(() => {
14
19
  return {