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

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,20 +1,16 @@
1
1
  {
2
2
  "name": "@kubb/plugin-zod",
3
- "version": "5.0.0-beta.3",
4
- "description": "Zod schema generator plugin for Kubb, creating type-safe validation schemas from OpenAPI specifications for runtime data validation.",
3
+ "version": "5.0.0-beta.31",
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
- "code-generator",
6
+ "code-generation",
7
7
  "codegen",
8
8
  "kubb",
9
- "oas",
10
9
  "openapi",
11
- "plugins",
10
+ "parse",
12
11
  "runtime-validation",
13
12
  "schema",
14
- "schema-validation",
15
13
  "swagger",
16
- "type-safe",
17
- "type-safety",
18
14
  "typescript",
19
15
  "validation",
20
16
  "zod"
@@ -30,7 +26,7 @@
30
26
  "src",
31
27
  "templates",
32
28
  "dist",
33
- "plugin.json",
29
+ "extension.yaml",
34
30
  "!/**/**.test.**",
35
31
  "!/**/__tests__/**",
36
32
  "!/**/__snapshots__/**"
@@ -52,15 +48,16 @@
52
48
  "registry": "https://registry.npmjs.org/"
53
49
  },
54
50
  "dependencies": {
55
- "@kubb/core": "5.0.0-beta.3",
56
- "@kubb/renderer-jsx": "5.0.0-beta.3",
57
- "remeda": "^2.34.0"
51
+ "@kubb/core": "5.0.0-beta.31",
52
+ "@kubb/renderer-jsx": "5.0.0-beta.31",
53
+ "remeda": "^2.34.1"
58
54
  },
59
55
  "devDependencies": {
56
+ "@internals/shared": "0.0.0",
60
57
  "@internals/utils": "0.0.0"
61
58
  },
62
59
  "peerDependencies": {
63
- "@kubb/renderer-jsx": "5.0.0-beta.3"
60
+ "@kubb/renderer-jsx": "5.0.0-beta.31"
64
61
  },
65
62
  "size-limit": [
66
63
  {
@@ -1,14 +1,14 @@
1
1
  import { stringifyObject } from '@internals/utils'
2
- import type { ast } from '@kubb/core'
2
+ import { ast } from '@kubb/core'
3
3
  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>
@@ -30,6 +30,7 @@ export function Operations({ name, operations }: Props): KubbReactNode {
30
30
  )
31
31
 
32
32
  const pathsJSON = operations.reduce<Record<string, Record<string, string>>>((prev, acc) => {
33
+ if (!ast.isHttpOperationNode(acc.node)) return prev
33
34
  prev[`"${acc.node.path}"`] = {
34
35
  ...(prev[`"${acc.node.path}"`] ?? {}),
35
36
  [acc.node.method]: `operations["${acc.node.operationId}"]`,
@@ -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 {
@@ -1,18 +1,60 @@
1
+ import { resolveContentTypeVariants } from '@internals/shared'
1
2
  import type { Adapter } from '@kubb/core'
2
3
  import { ast, defineGenerator } from '@kubb/core'
3
4
  import type { AdapterOas } from '@kubb/adapter-oas'
4
- import { File, jsxRenderer } from '@kubb/renderer-jsx'
5
+ import { File, jsxRendererSync } from '@kubb/renderer-jsx'
5
6
  import { Operations } from '../components/Operations.tsx'
6
7
  import { Zod } from '../components/Zod.tsx'
7
8
  import { ZOD_NAMESPACE_IMPORTS } from '../constants.ts'
8
9
  import { printerZod } from '../printers/printerZod.ts'
9
10
  import { printerZodMini } from '../printers/printerZodMini.ts'
10
- import type { PluginZod } from '../types'
11
- import { buildSchemaNames } from '../utils.ts'
12
-
11
+ import type { PluginZod, ResolverZod } from '../types'
12
+ import { buildSchemaNames, containsCodec } from '../utils.ts'
13
+
14
+ type StdPrinters = { output: ReturnType<typeof printerZod>; input: ReturnType<typeof printerZod> }
15
+ type ZodPrinterEntry = StdPrinters & { coercion: unknown; guidType: unknown; dateType: unknown }
16
+ type ZodMiniPrinterEntry = { printer: ReturnType<typeof printerZodMini>; guidType: unknown }
17
+
18
+ // Per-build caches: keyed on resolver (unique per plugin instance per build, GC'd when released)
19
+ const zodPrinterCache = new WeakMap<ResolverZod, ZodPrinterEntry>()
20
+ const zodMiniPrinterCache = new WeakMap<ResolverZod, ZodMiniPrinterEntry>()
21
+
22
+ type StdPrinterParams = { coercion: unknown; guidType: unknown; dateType: unknown; wrapOutput: unknown; cyclicSchemas: ReadonlySet<string>; nodes: unknown }
23
+
24
+ /**
25
+ * Returns the cached `output`/`input` direction printers for a resolver, building them on
26
+ * first use. The `input` printer encodes `Date → string` for request bodies; `output` decodes
27
+ * `string → Date` for responses. Schemas without `dateType: 'date'` fields print identically.
28
+ */
29
+ function getStdPrinters(resolver: ResolverZod, params: StdPrinterParams): StdPrinters {
30
+ const cached = zodPrinterCache.get(resolver)
31
+ if (cached && cached.coercion === params.coercion && cached.guidType === params.guidType && cached.dateType === params.dateType) {
32
+ return { output: cached.output, input: cached.input }
33
+ }
34
+ const base = { ...params, resolver } as Parameters<typeof printerZod>[0]
35
+ const output = printerZod({ ...base, direction: 'output' })
36
+ const input = printerZod({ ...base, direction: 'input' })
37
+ zodPrinterCache.set(resolver, { output, input, coercion: params.coercion, guidType: params.guidType, dateType: params.dateType })
38
+ return { output, input }
39
+ }
40
+
41
+ function getMiniPrinter(resolver: ResolverZod, params: { guidType: unknown; wrapOutput: unknown; cyclicSchemas: ReadonlySet<string>; nodes: unknown }) {
42
+ const cached = zodMiniPrinterCache.get(resolver)
43
+ if (cached && cached.guidType === params.guidType) return cached.printer
44
+ const p = printerZodMini({ ...params, resolver } as Parameters<typeof printerZodMini>[0])
45
+ zodMiniPrinterCache.set(resolver, { printer: p, guidType: params.guidType })
46
+ return p
47
+ }
48
+
49
+ /**
50
+ * Built-in generator for `@kubb/plugin-zod`. Emits one Zod schema per
51
+ * schema in the spec plus per-operation request/response/parameter schemas.
52
+ * When `mini: true`, schemas use the Zod Mini functional API instead of
53
+ * chainable methods.
54
+ */
13
55
  export const zodGenerator = defineGenerator<PluginZod>({
14
56
  name: 'zod',
15
- renderer: jsxRenderer,
57
+ renderer: jsxRendererSync,
16
58
  schema(node, ctx) {
17
59
  const { adapter, config, resolver, root } = ctx
18
60
  const { output, coercion, guidType, mini, wrapOutput, inferred, importPath, group, printer } = ctx.options
@@ -24,41 +66,73 @@ export const zodGenerator = defineGenerator<PluginZod>({
24
66
 
25
67
  const mode = ctx.getMode(output)
26
68
  const isZodImport = ZOD_NAMESPACE_IMPORTS.has(importPath as 'zod' | 'zod/mini')
27
-
28
- const imports = adapter.getImports(node, (schemaName) => ({
69
+ const cyclicSchemas = new Set<string>(ctx.meta.circularNames)
70
+
71
+ // A codec component is rendered twice: the canonical (output) schema decodes
72
+ // `string → Date`, and an `${name}InputSchema` variant encodes `Date → string` for requests.
73
+ const hasCodec = !mini && containsCodec(node)
74
+
75
+ const codecRefNames = new Set(
76
+ hasCodec
77
+ ? ast.collect<string>(node, {
78
+ schema: (n) => (n.type === 'ref' && n.ref && containsCodec(n) ? (ast.extractRefName(n.ref) ?? undefined) : undefined),
79
+ })
80
+ : [],
81
+ )
82
+ const importEntries = adapter.getImports(node, (schemaName) => ({
29
83
  name: resolver.resolveSchemaName(schemaName),
30
- path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }).path,
84
+ path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group: group ?? undefined }).path,
31
85
  }))
86
+ const inputImportEntries = hasCodec
87
+ ? [...codecRefNames].map((schemaName) => ({
88
+ name: resolver.resolveInputSchemaName(schemaName),
89
+ path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group: group ?? undefined }).path,
90
+ }))
91
+ : []
92
+ const seenImports = new Set<string>()
93
+ const imports = [...importEntries, ...inputImportEntries].filter((imp) => {
94
+ const key = `${Array.isArray(imp.name) ? imp.name.join(',') : imp.name}|${imp.path}`
95
+ if (seenImports.has(key)) return false
96
+ seenImports.add(key)
97
+ return true
98
+ })
32
99
 
33
100
  const meta = {
34
101
  name: resolver.resolveSchemaName(node.name),
35
- file: resolver.resolveFile({ name: node.name, extname: '.ts' }, { root, output, group }),
102
+ file: resolver.resolveFile({ name: node.name, extname: '.ts' }, { root, output, group: group ?? undefined }),
36
103
  } as const
37
104
 
38
- const inferTypeName = inferred ? resolver.resolveSchemaTypeName(node.name) : undefined
39
-
40
- const cyclicSchemas = adapter.inputNode ? ast.findCircularSchemas(adapter.inputNode.schemas) : undefined
105
+ const inferTypeName = inferred ? resolver.resolveSchemaTypeName(node.name) : null
41
106
 
42
- const schemaPrinter = mini
43
- ? printerZodMini({ guidType, wrapOutput, resolver, cyclicSchemas, nodes: printer?.nodes })
44
- : printerZod({ coercion, guidType, dateType, wrapOutput, resolver, cyclicSchemas, nodes: printer?.nodes })
107
+ const stdPrinters = mini ? null : getStdPrinters(resolver, { coercion, guidType, dateType, wrapOutput, cyclicSchemas, nodes: printer?.nodes })
108
+ const schemaPrinter = mini ? getMiniPrinter(resolver, { guidType, wrapOutput, cyclicSchemas, nodes: printer?.nodes }) : stdPrinters!.output
45
109
 
46
110
  return (
47
111
  <File
48
112
  baseName={meta.file.baseName}
49
113
  path={meta.file.path}
50
114
  meta={meta.file.meta}
51
- banner={resolver.resolveBanner(adapter.inputNode, { output, config })}
52
- footer={resolver.resolveFooter(adapter.inputNode, { output, config })}
115
+ banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
116
+ footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
53
117
  >
54
118
  <File.Import name={isZodImport ? 'z' : ['z']} path={importPath} isNameSpace={isZodImport} />
55
- {mode === 'split' && imports.map((imp) => <File.Import key={[node.name, imp.path].join('-')} root={meta.file.path} path={imp.path} name={imp.name} />)}
119
+ {mode === 'split' &&
120
+ imports.map((imp) => <File.Import key={[node.name, imp.path, imp.name].join('-')} root={meta.file.path} path={imp.path} name={imp.name} />)}
56
121
 
57
122
  <Zod name={meta.name} node={node} printer={schemaPrinter} inferTypeName={inferTypeName} />
123
+ {hasCodec && stdPrinters && (
124
+ <Zod
125
+ name={resolver.resolveInputSchemaName(node.name)}
126
+ node={node}
127
+ printer={stdPrinters.input}
128
+ inferTypeName={inferred ? resolver.resolveInputSchemaTypeName(node.name) : null}
129
+ />
130
+ )}
58
131
  </File>
59
132
  )
60
133
  },
61
134
  operation(node, ctx) {
135
+ if (!ast.isHttpOperationNode(node)) return null
62
136
  const { adapter, config, resolver, root } = ctx
63
137
  const { output, coercion, guidType, mini, wrapOutput, inferred, importPath, group, paramsCasing, printer } = ctx.options
64
138
  const dateType = (adapter as Adapter<AdapterOas>).options.dateType
@@ -69,24 +143,50 @@ export const zodGenerator = defineGenerator<PluginZod>({
69
143
  const params = ast.caseParams(node.parameters, paramsCasing)
70
144
 
71
145
  const meta = {
72
- file: resolver.resolveFile({ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path }, { root, output, group }),
146
+ file: resolver.resolveFile(
147
+ { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
148
+ { root, output, group: group ?? undefined },
149
+ ),
73
150
  } as const
74
151
 
75
- const cyclicSchemas = adapter.inputNode ? ast.findCircularSchemas(adapter.inputNode.schemas) : undefined
76
-
77
- function renderSchemaEntry({ schema, name, keysToOmit }: { schema: ast.SchemaNode | null; name: string; keysToOmit?: Array<string> }) {
152
+ const cyclicSchemas = new Set<string>(ctx.meta.circularNames)
153
+
154
+ function renderSchemaEntry({
155
+ schema,
156
+ name,
157
+ keysToOmit,
158
+ direction = 'output',
159
+ }: {
160
+ schema: ast.SchemaNode | null
161
+ name: string
162
+ keysToOmit?: Array<string> | null
163
+ direction?: 'input' | 'output'
164
+ }) {
78
165
  if (!schema) return null
79
166
 
80
- const inferTypeName = inferred ? resolver.resolveTypeName(name) : undefined
167
+ const inferTypeName = inferred ? resolver.resolveTypeName(name) : null
81
168
 
169
+ // In the input direction, refs to codec components resolve to their input variant.
170
+ const codecRefNames =
171
+ direction === 'input' && !mini
172
+ ? new Set(
173
+ ast.collect<string>(schema, {
174
+ schema: (n) => (n.type === 'ref' && n.ref && containsCodec(n) ? (ast.extractRefName(n.ref) ?? undefined) : undefined),
175
+ }),
176
+ )
177
+ : null
82
178
  const imports = adapter.getImports(schema, (schemaName) => ({
83
- name: resolver.resolveSchemaName(schemaName),
84
- path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }).path,
179
+ name: codecRefNames?.has(schemaName) ? resolver.resolveInputSchemaName(schemaName) : resolver.resolveSchemaName(schemaName),
180
+ path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group: group ?? undefined }).path,
85
181
  }))
86
182
 
87
183
  const schemaPrinter = mini
88
- ? printerZodMini({ guidType, wrapOutput, resolver, keysToOmit, cyclicSchemas, nodes: printer?.nodes })
89
- : printerZod({ coercion, guidType, dateType, wrapOutput, resolver, keysToOmit, cyclicSchemas, nodes: printer?.nodes })
184
+ ? keysToOmit?.length
185
+ ? printerZodMini({ guidType, wrapOutput, resolver, keysToOmit, cyclicSchemas, nodes: printer?.nodes })
186
+ : getMiniPrinter(resolver, { guidType, wrapOutput, cyclicSchemas, nodes: printer?.nodes })
187
+ : keysToOmit?.length
188
+ ? printerZod({ coercion, guidType, dateType, wrapOutput, resolver, keysToOmit, cyclicSchemas, nodes: printer?.nodes, direction })
189
+ : getStdPrinters(resolver, { coercion, guidType, dateType, wrapOutput, cyclicSchemas, nodes: printer?.nodes })[direction]
90
190
 
91
191
  return (
92
192
  <>
@@ -97,17 +197,49 @@ export const zodGenerator = defineGenerator<PluginZod>({
97
197
  )
98
198
  }
99
199
 
100
- const paramSchemas = params.map((param) => renderSchemaEntry({ schema: param.schema, name: resolver.resolveParamName(node, param) }))
200
+ // Multiple content types for a single name — emit one schema per content type plus a union alias.
201
+ function buildContentTypeVariants(
202
+ entries: Array<{ contentType: string; schema?: ast.SchemaNode | null; keysToOmit?: Array<string> | null }>,
203
+ baseName: string,
204
+ decorate?: (schema: ast.SchemaNode) => ast.SchemaNode,
205
+ direction?: 'input' | 'output',
206
+ ) {
207
+ const variants = resolveContentTypeVariants(entries, baseName)
208
+ const unionSchema = ast.createSchema({
209
+ type: 'union',
210
+ members: variants.map((variant) => ast.createSchema({ type: 'ref', name: variant.name })),
211
+ })
212
+ return (
213
+ <>
214
+ {variants.map((variant) =>
215
+ renderSchemaEntry({
216
+ schema: decorate ? decorate(variant.schema) : variant.schema,
217
+ name: variant.name,
218
+ keysToOmit: variant.keysToOmit,
219
+ direction,
220
+ }),
221
+ )}
222
+ {renderSchemaEntry({ schema: unionSchema, name: baseName, direction })}
223
+ </>
224
+ )
225
+ }
226
+
227
+ const paramSchemas = params.map((param) => renderSchemaEntry({ schema: param.schema, name: resolver.resolveParamName(node, param), direction: 'input' }))
101
228
 
102
- const responseSchemas = node.responses.map((res) =>
103
- renderSchemaEntry({
104
- schema: res.schema,
229
+ const responseSchemas = node.responses.map((res) => {
230
+ const variants = (res.content ?? []).filter((entry) => entry.schema)
231
+ if (variants.length > 1) {
232
+ return buildContentTypeVariants(res.content!, resolver.resolveResponseStatusName(node, res.statusCode))
233
+ }
234
+ const primary = variants[0] ?? res.content?.[0]
235
+ return renderSchemaEntry({
236
+ schema: primary?.schema ?? null,
105
237
  name: resolver.resolveResponseStatusName(node, res.statusCode),
106
- keysToOmit: res.keysToOmit,
107
- }),
108
- )
238
+ keysToOmit: primary?.keysToOmit,
239
+ })
240
+ })
109
241
 
110
- const responsesWithSchema = node.responses.filter((res) => res.schema)
242
+ const responsesWithSchema = node.responses.filter((res) => res.content?.some((entry) => entry.schema))
111
243
  const responseUnionSchema =
112
244
  responsesWithSchema.length > 0
113
245
  ? (() => {
@@ -118,14 +250,16 @@ export const zodGenerator = defineGenerator<PluginZod>({
118
250
  // the response union name, skip generation to avoid redeclaration errors.
119
251
  const importedNames = new Set(
120
252
  responsesWithSchema.flatMap((res) =>
121
- res.schema
122
- ? adapter
123
- .getImports(res.schema, (schemaName) => ({
124
- name: resolver.resolveSchemaName(schemaName),
125
- path: '',
126
- }))
127
- .flatMap((imp) => (Array.isArray(imp.name) ? imp.name : [imp.name]))
128
- : [],
253
+ (res.content ?? []).flatMap((entry) =>
254
+ entry.schema
255
+ ? adapter
256
+ .getImports(entry.schema, (schemaName) => ({
257
+ name: resolver.resolveSchemaName(schemaName),
258
+ path: '',
259
+ }))
260
+ .flatMap((imp) => (Array.isArray(imp.name) ? imp.name : [imp.name]))
261
+ : [],
262
+ ),
129
263
  ),
130
264
  )
131
265
 
@@ -143,24 +277,37 @@ export const zodGenerator = defineGenerator<PluginZod>({
143
277
  })()
144
278
  : null
145
279
 
146
- const requestSchema = node.requestBody?.content?.[0]?.schema
147
- ? renderSchemaEntry({
148
- schema: {
149
- ...node.requestBody.content![0]!.schema!,
150
- description: node.requestBody.description ?? node.requestBody.content![0]!.schema!.description,
151
- },
280
+ const requestBodyContent = node.requestBody?.content ?? []
281
+ const requestSchema = (() => {
282
+ if (requestBodyContent.length === 0) return null
283
+ if (requestBodyContent.length === 1) {
284
+ const entry = requestBodyContent[0]!
285
+ if (!entry.schema) return null
286
+ return renderSchemaEntry({
287
+ schema: { ...entry.schema, description: node.requestBody!.description ?? entry.schema.description },
152
288
  name: resolver.resolveDataName(node),
153
- keysToOmit: node.requestBody.content![0]!.keysToOmit,
289
+ keysToOmit: entry.keysToOmit,
290
+ direction: 'input',
154
291
  })
155
- : null
292
+ }
293
+ return buildContentTypeVariants(
294
+ requestBodyContent,
295
+ resolver.resolveDataName(node),
296
+ (schema) => ({
297
+ ...schema,
298
+ description: node.requestBody!.description ?? schema.description,
299
+ }),
300
+ 'input',
301
+ )
302
+ })()
156
303
 
157
304
  return (
158
305
  <File
159
306
  baseName={meta.file.baseName}
160
307
  path={meta.file.path}
161
308
  meta={meta.file.meta}
162
- banner={resolver.resolveBanner(adapter.inputNode, { output, config })}
163
- footer={resolver.resolveFooter(adapter.inputNode, { output, config })}
309
+ banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
310
+ footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
164
311
  >
165
312
  <File.Import name={isZodImport ? 'z' : ['z']} path={importPath} isNameSpace={isZodImport} />
166
313
  {paramSchemas}
@@ -171,7 +318,7 @@ export const zodGenerator = defineGenerator<PluginZod>({
171
318
  )
172
319
  },
173
320
  operations(nodes, ctx) {
174
- const { adapter, config, resolver, root } = ctx
321
+ const { config, resolver, root } = ctx
175
322
  const { output, importPath, group, operations, paramsCasing } = ctx.options
176
323
 
177
324
  if (!operations) {
@@ -180,10 +327,10 @@ export const zodGenerator = defineGenerator<PluginZod>({
180
327
  const isZodImport = ZOD_NAMESPACE_IMPORTS.has(importPath as 'zod' | 'zod/mini')
181
328
 
182
329
  const meta = {
183
- file: resolver.resolveFile({ name: 'operations', extname: '.ts' }, { root, output, group }),
330
+ file: resolver.resolveFile({ name: 'operations', extname: '.ts' }, { root, output, group: group ?? undefined }),
184
331
  } as const
185
332
 
186
- const transformedOperations = nodes.map((node) => {
333
+ const transformedOperations = nodes.filter(ast.isHttpOperationNode).map((node) => {
187
334
  const params = ast.caseParams(node.parameters, paramsCasing)
188
335
 
189
336
  return {
@@ -193,8 +340,11 @@ export const zodGenerator = defineGenerator<PluginZod>({
193
340
  })
194
341
 
195
342
  const imports = transformedOperations.flatMap(({ node, data }) => {
196
- const names = [data.request, ...Object.values(data.responses), ...Object.values(data.parameters)].filter(Boolean) as string[]
197
- const opFile = resolver.resolveFile({ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path }, { root, output, group })
343
+ const names = [data.request, ...Object.values(data.responses), ...Object.values(data.parameters)].filter(Boolean) as Array<string>
344
+ const opFile = resolver.resolveFile(
345
+ { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
346
+ { root, output, group: group ?? undefined },
347
+ )
198
348
 
199
349
  return names.map((name) => <File.Import key={[name, opFile.path].join('-')} name={[name]} root={meta.file.path} path={opFile.path} />)
200
350
  })
@@ -204,8 +354,8 @@ export const zodGenerator = defineGenerator<PluginZod>({
204
354
  baseName={meta.file.baseName}
205
355
  path={meta.file.path}
206
356
  meta={meta.file.meta}
207
- banner={resolver.resolveBanner(adapter.inputNode, { output, config })}
208
- footer={resolver.resolveFooter(adapter.inputNode, { output, config })}
357
+ banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
358
+ footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
209
359
  >
210
360
  <File.Import isTypeOnly name={isZodImport ? 'z' : ['z']} path={importPath} isNameSpace={isZodImport} />
211
361
  {imports}
package/src/plugin.ts CHANGED
@@ -1,24 +1,37 @@
1
- import { camelCase } from '@internals/utils'
2
- import { definePlugin, type Group } from '@kubb/core'
1
+ import { createGroupConfig } from '@internals/shared'
2
+ import { definePlugin } from '@kubb/core'
3
3
  import { zodGenerator } from './generators/zodGenerator.tsx'
4
4
  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
  */
@@ -44,17 +57,7 @@ export const pluginZod = definePlugin<PluginZod>((options) => {
44
57
  generators: userGenerators = [],
45
58
  } = options
46
59
 
47
- const groupConfig = group
48
- ? ({
49
- ...group,
50
- name: (ctx) => {
51
- if (group.type === 'path') {
52
- return `${ctx.group.split('/')[1]}`
53
- }
54
- return `${camelCase(ctx.group)}Controller`
55
- },
56
- } satisfies Group)
57
- : undefined
60
+ const groupConfig = createGroupConfig(group, { suffix: 'Controller' })
58
61
 
59
62
  return {
60
63
  name: pluginZodName,