@kubb/plugin-zod 5.0.0-alpha.9 → 5.0.0-beta.3

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.
Files changed (45) hide show
  1. package/LICENSE +17 -10
  2. package/README.md +1 -3
  3. package/dist/index.cjs +1061 -105
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.ts +369 -4
  6. package/dist/index.js +1053 -104
  7. package/dist/index.js.map +1 -1
  8. package/package.json +44 -70
  9. package/src/components/Operations.tsx +25 -18
  10. package/src/components/Zod.tsx +21 -121
  11. package/src/constants.ts +5 -0
  12. package/src/generators/zodGenerator.tsx +174 -160
  13. package/src/index.ts +11 -2
  14. package/src/plugin.ts +67 -156
  15. package/src/printers/printerZod.ts +339 -0
  16. package/src/printers/printerZodMini.ts +295 -0
  17. package/src/resolvers/resolverZod.ts +57 -0
  18. package/src/types.ts +130 -115
  19. package/src/utils.ts +222 -0
  20. package/dist/components-B7zUFnAm.cjs +0 -890
  21. package/dist/components-B7zUFnAm.cjs.map +0 -1
  22. package/dist/components-eECfXVou.js +0 -842
  23. package/dist/components-eECfXVou.js.map +0 -1
  24. package/dist/components.cjs +0 -4
  25. package/dist/components.d.ts +0 -56
  26. package/dist/components.js +0 -2
  27. package/dist/generators-BjPDdJUz.cjs +0 -301
  28. package/dist/generators-BjPDdJUz.cjs.map +0 -1
  29. package/dist/generators-lTWPS6oN.js +0 -290
  30. package/dist/generators-lTWPS6oN.js.map +0 -1
  31. package/dist/generators.cjs +0 -4
  32. package/dist/generators.d.ts +0 -508
  33. package/dist/generators.js +0 -2
  34. package/dist/templates/ToZod.source.cjs +0 -7
  35. package/dist/templates/ToZod.source.cjs.map +0 -1
  36. package/dist/templates/ToZod.source.d.ts +0 -7
  37. package/dist/templates/ToZod.source.js +0 -6
  38. package/dist/templates/ToZod.source.js.map +0 -1
  39. package/dist/types-CoCoOc2u.d.ts +0 -172
  40. package/src/components/index.ts +0 -2
  41. package/src/generators/index.ts +0 -2
  42. package/src/generators/operationsGenerator.tsx +0 -50
  43. package/src/parser.ts +0 -909
  44. package/src/templates/ToZod.source.ts +0 -4
  45. package/templates/ToZod.ts +0 -61
@@ -0,0 +1,339 @@
1
+ import { stringify } from '@internals/utils'
2
+
3
+ import { ast } from '@kubb/core'
4
+ import type { PluginZod, ResolverZod } from '../types.ts'
5
+ import { applyModifiers, formatLiteral, lengthConstraints, numberConstraints, shouldCoerce } from '../utils.ts'
6
+ import type { AdapterOas } from '@kubb/adapter-oas'
7
+
8
+ /**
9
+ * Partial map of node-type overrides for the Zod printer.
10
+ *
11
+ * Each key is a `SchemaType` string (e.g. `'date'`, `'string'`). The function
12
+ * replaces the built-in handler for that node type. Use `this.transform` to
13
+ * recurse into nested schema nodes, and `this.options` to read printer options.
14
+ *
15
+ * @example Override the `date` handler
16
+ * ```ts
17
+ * pluginZod({
18
+ * printer: {
19
+ * nodes: {
20
+ * date(node) {
21
+ * return 'z.string().date()'
22
+ * },
23
+ * },
24
+ * },
25
+ * })
26
+ * ```
27
+ */
28
+ export type PrinterZodNodes = ast.PrinterPartial<string, PrinterZodOptions>
29
+
30
+ export type PrinterZodOptions = {
31
+ /**
32
+ * Enable automatic type coercion for strings, numbers, and dates.
33
+ */
34
+ coercion?: PluginZod['resolvedOptions']['coercion']
35
+ /**
36
+ * Use `z.guid()` or `z.uuid()` for UUID/GUID validation.
37
+ *
38
+ * @default 'uuid'
39
+ */
40
+ guidType?: PluginZod['resolvedOptions']['guidType']
41
+ /**
42
+ * Date format in the OpenAPI spec (`'date'` or `'date-time'`).
43
+ */
44
+ dateType?: AdapterOas['resolvedOptions']['dateType']
45
+ /**
46
+ * Hook to transform generated Zod schema before output.
47
+ */
48
+ wrapOutput?: PluginZod['resolvedOptions']['wrapOutput']
49
+ /**
50
+ * Transforms raw schema names into valid JavaScript identifiers.
51
+ */
52
+ resolver?: ResolverZod
53
+ /**
54
+ * Properties to exclude using `.omit({ key: true })`.
55
+ */
56
+ keysToOmit?: Array<string>
57
+ /**
58
+ * Schema names that form circular dependency chains.
59
+ * Properties referencing these emit lazy getters wrapping refs in `z.lazy(() => …)`.
60
+ */
61
+ cyclicSchemas?: ReadonlySet<string>
62
+ /**
63
+ * Custom handler map for node type overrides.
64
+ */
65
+ nodes?: PrinterZodNodes
66
+ }
67
+
68
+ /**
69
+ * Factory options for the Zod printer, defining input/output types and configuration.
70
+ */
71
+ export type PrinterZodFactory = ast.PrinterFactoryOptions<'zod', PrinterZodOptions, string, string>
72
+
73
+ /**
74
+ * Zod v4 printer built with `definePrinter`.
75
+ *
76
+ * Converts a `SchemaNode` AST into a Zod v4 code string using the chainable API
77
+ * (`.optional()`, `.nullable()`, `.omit()`, etc.). For improved tree-shaking, see {@link printerZodMini}.
78
+ *
79
+ * @example Chainable API
80
+ * ```ts
81
+ * const printer = printerZod({ coercion: false })
82
+ * const code = printer.print(stringNode) // "z.string()"
83
+ * ```
84
+ */
85
+ export const printerZod = ast.definePrinter<PrinterZodFactory>((options) => {
86
+ return {
87
+ name: 'zod',
88
+ options,
89
+ nodes: {
90
+ any: () => 'z.any()',
91
+ unknown: () => 'z.unknown()',
92
+ void: () => 'z.void()',
93
+ never: () => 'z.never()',
94
+ boolean: () => 'z.boolean()',
95
+ null: () => 'z.null()',
96
+ string(node) {
97
+ const base = shouldCoerce(this.options.coercion, 'strings') ? 'z.coerce.string()' : 'z.string()'
98
+
99
+ return `${base}${lengthConstraints(node)}`
100
+ },
101
+ number(node) {
102
+ const base = shouldCoerce(this.options.coercion, 'numbers') ? 'z.coerce.number()' : 'z.number()'
103
+
104
+ return `${base}${numberConstraints(node)}`
105
+ },
106
+ integer(node) {
107
+ const base = shouldCoerce(this.options.coercion, 'numbers') ? 'z.coerce.number().int()' : 'z.int()'
108
+
109
+ return `${base}${numberConstraints(node)}`
110
+ },
111
+ bigint() {
112
+ return shouldCoerce(this.options.coercion, 'numbers') ? 'z.coerce.bigint()' : 'z.bigint()'
113
+ },
114
+ date(node) {
115
+ if (node.representation === 'string') {
116
+ return 'z.iso.date()'
117
+ }
118
+
119
+ return shouldCoerce(this.options.coercion, 'dates') ? 'z.coerce.date()' : 'z.date()'
120
+ },
121
+ datetime(node) {
122
+ const offset = node.offset || this.options.dateType === 'stringOffset'
123
+ const local = node.local || this.options.dateType === 'stringLocal'
124
+
125
+ if (offset) return 'z.iso.datetime({ offset: true })'
126
+ if (local) return 'z.iso.datetime({ local: true })'
127
+
128
+ return 'z.iso.datetime()'
129
+ },
130
+ time(node) {
131
+ if (node.representation === 'string') {
132
+ return 'z.iso.time()'
133
+ }
134
+
135
+ return shouldCoerce(this.options.coercion, 'dates') ? 'z.coerce.date()' : 'z.date()'
136
+ },
137
+ uuid(node) {
138
+ const base = this.options.guidType === 'guid' ? 'z.guid()' : 'z.uuid()'
139
+
140
+ return `${base}${lengthConstraints(node)}`
141
+ },
142
+ email(node) {
143
+ return `z.email()${lengthConstraints(node)}`
144
+ },
145
+ url(node) {
146
+ return `z.url()${lengthConstraints(node)}`
147
+ },
148
+ ipv4: () => 'z.ipv4()',
149
+ ipv6: () => 'z.ipv6()',
150
+ blob: () => 'z.instanceof(File)',
151
+ enum(node) {
152
+ const values = node.namedEnumValues?.map((v) => v.value) ?? node.enumValues ?? []
153
+ const nonNullValues = values.filter((v): v is string | number | boolean => v !== null)
154
+
155
+ // asConst-style enum: use z.union([z.literal(…), …])
156
+ if (node.namedEnumValues?.length) {
157
+ const literals = nonNullValues.map((v) => `z.literal(${formatLiteral(v)})`)
158
+
159
+ if (literals.length === 1) return literals[0]!
160
+ return `z.union([${literals.join(', ')}])`
161
+ }
162
+
163
+ // Regular enum: use z.enum([…])
164
+ return `z.enum([${nonNullValues.map(formatLiteral).join(', ')}])`
165
+ },
166
+ ref(node) {
167
+ if (!node.name) return undefined
168
+ const refName = node.ref ? (ast.extractRefName(node.ref) ?? node.name) : node.name
169
+ const resolvedName = node.ref ? (this.options.resolver?.default(refName, 'function') ?? refName) : node.name
170
+
171
+ if (node.ref && this.options.cyclicSchemas?.has(refName)) {
172
+ return `z.lazy(() => ${resolvedName})`
173
+ }
174
+
175
+ return resolvedName
176
+ },
177
+ object(node) {
178
+ const properties = node.properties
179
+ .map((prop) => {
180
+ const { name: propName, schema } = prop
181
+
182
+ const meta = ast.syncSchemaRef(schema)
183
+
184
+ const isNullable = meta.nullable
185
+ const isOptional = schema.optional
186
+ const isNullish = schema.nullish
187
+
188
+ const hasSelfRef = this.options.cyclicSchemas != null && ast.containsCircularRef(schema, { circularSchemas: this.options.cyclicSchemas })
189
+ // Inside a getter the getter itself defers evaluation, so suppress
190
+ // z.lazy() wrapping on nested refs by temporarily clearing cyclicSchemas.
191
+ if (hasSelfRef) this.options.cyclicSchemas = undefined
192
+ const baseOutput = this.transform(schema) ?? this.transform(ast.createSchema({ type: 'unknown' }))!
193
+ if (hasSelfRef) this.options.cyclicSchemas = options.cyclicSchemas
194
+
195
+ const wrappedOutput = this.options.wrapOutput ? this.options.wrapOutput({ output: baseOutput, schema }) || baseOutput : baseOutput
196
+
197
+ // When a property schema is not a ref but the metadata is from a ref (e.g., discriminator
198
+ // property override), skip applying the description from the ref target to avoid applying
199
+ // metadata from a replaced schema.
200
+ let descriptionToApply = meta.description
201
+ if (schema.type !== 'ref' && meta.type === 'ref') {
202
+ descriptionToApply = undefined
203
+ }
204
+
205
+ const value = applyModifiers({
206
+ value: wrappedOutput,
207
+ nullable: isNullable,
208
+ optional: isOptional,
209
+ nullish: isNullish,
210
+ defaultValue: meta.default,
211
+ description: descriptionToApply,
212
+ })
213
+
214
+ if (hasSelfRef) {
215
+ return `get "${propName}"() { return ${value} }`
216
+ }
217
+ return `"${propName}": ${value}`
218
+ })
219
+ .join(',\n ')
220
+
221
+ let result = `z.object({\n ${properties}\n })`
222
+
223
+ // Handle additionalProperties as .catchall() or .strict()
224
+ if (node.additionalProperties && node.additionalProperties !== true) {
225
+ const catchallType = this.transform(node.additionalProperties)
226
+ if (catchallType) {
227
+ result += `.catchall(${catchallType})`
228
+ }
229
+ } else if (node.additionalProperties === true) {
230
+ result += `.catchall(${this.transform(ast.createSchema({ type: 'unknown' }))})`
231
+ } else if (node.additionalProperties === false) {
232
+ result += '.strict()'
233
+ }
234
+
235
+ return result
236
+ },
237
+ array(node) {
238
+ const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
239
+ const inner = items.join(', ') || this.transform(ast.createSchema({ type: 'unknown' }))!
240
+ let result = `z.array(${inner})${lengthConstraints(node)}`
241
+
242
+ if (node.unique) {
243
+ result += `.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })`
244
+ }
245
+
246
+ return result
247
+ },
248
+ tuple(node) {
249
+ const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
250
+
251
+ return `z.tuple([${items.join(', ')}])`
252
+ },
253
+ union(node) {
254
+ const nodeMembers = node.members ?? []
255
+ const members = nodeMembers.map((m) => this.transform(m)).filter(Boolean)
256
+ if (members.length === 0) return ''
257
+ if (members.length === 1) return members[0]!
258
+ if (node.discriminatorPropertyName && !nodeMembers.some((m) => m.type === 'intersection')) {
259
+ // z.discriminatedUnion requires ZodObject members; intersections (ZodIntersection) are not
260
+ // assignable to $ZodDiscriminant, so fall back to z.union when any member is an intersection.
261
+ return `z.discriminatedUnion(${stringify(node.discriminatorPropertyName)}, [${members.join(', ')}])`
262
+ }
263
+
264
+ return `z.union([${members.join(', ')}])`
265
+ },
266
+ intersection(node) {
267
+ const members = node.members ?? []
268
+ if (members.length === 0) return ''
269
+
270
+ const [first, ...rest] = members
271
+ if (!first) return ''
272
+
273
+ let base = this.transform(first)
274
+ if (!base) return ''
275
+
276
+ for (const member of rest) {
277
+ if (member.primitive === 'string') {
278
+ const s = ast.narrowSchema(member, 'string')
279
+ const c = lengthConstraints(s ?? {})
280
+ if (c) {
281
+ base += c
282
+ continue
283
+ }
284
+ } else if (member.primitive === 'number' || member.primitive === 'integer') {
285
+ const n = ast.narrowSchema(member, 'number') ?? ast.narrowSchema(member, 'integer')
286
+ const c = numberConstraints(n ?? {})
287
+ if (c) {
288
+ base += c
289
+ continue
290
+ }
291
+ } else if (member.primitive === 'array') {
292
+ const a = ast.narrowSchema(member, 'array')
293
+ const c = lengthConstraints(a ?? {})
294
+ if (c) {
295
+ base += c
296
+ continue
297
+ }
298
+ }
299
+ const transformed = this.transform(member)
300
+ if (transformed) base = `${base}.and(${transformed})`
301
+ }
302
+
303
+ return base
304
+ },
305
+ ...options.nodes,
306
+ },
307
+ print(node) {
308
+ const { keysToOmit } = this.options
309
+
310
+ let base = this.transform(node)
311
+ if (!base) return null
312
+
313
+ const meta = ast.syncSchemaRef(node)
314
+
315
+ if (keysToOmit?.length && meta.primitive === 'object' && !(meta.type === 'union' && meta.discriminatorPropertyName)) {
316
+ // Mirror printerTs `nonNullable: true`: when omitting keys, the resulting
317
+ // schema is a new non-nullable object type — skip optional/nullable/nullish.
318
+ // Discriminated unions (z.discriminatedUnion) do not support .omit(), so skip them.
319
+
320
+ // If this is a lazy reference, apply omit inside the lazy function
321
+ const lazyMatch = base.match(/^z\.lazy\(\(\)\s*=>\s*(.+)\)$/)
322
+ if (lazyMatch) {
323
+ base = `z.lazy(() => ${lazyMatch[1]}.omit({ ${keysToOmit.map((k: string) => `"${k}": true`).join(', ')} }))`
324
+ } else {
325
+ base = `${base}.omit({ ${keysToOmit.map((k: string) => `"${k}": true`).join(', ')} })`
326
+ }
327
+ }
328
+
329
+ return applyModifiers({
330
+ value: base,
331
+ nullable: meta.nullable,
332
+ optional: meta.optional,
333
+ nullish: meta.nullish,
334
+ defaultValue: meta.default,
335
+ description: meta.description,
336
+ })
337
+ },
338
+ }
339
+ })
@@ -0,0 +1,295 @@
1
+ import { stringify } from '@internals/utils'
2
+
3
+ import { ast } from '@kubb/core'
4
+ import type { PluginZod, ResolverZod } from '../types.ts'
5
+ import { applyMiniModifiers, formatLiteral, lengthChecksMini, numberChecksMini } from '../utils.ts'
6
+
7
+ /**
8
+ * Partial map of node-type overrides for the Zod Mini printer.
9
+ *
10
+ * Each key is a `SchemaType` string (e.g. `'date'`, `'string'`). The function
11
+ * replaces the built-in handler for that node type. Use `this.transform` to
12
+ * recurse into nested schema nodes, and `this.options` to read printer options.
13
+ *
14
+ * @example Override the `date` handler
15
+ * ```ts
16
+ * pluginZod({
17
+ * mini: true,
18
+ * printer: {
19
+ * nodes: {
20
+ * date(node) {
21
+ * return 'z.iso.date()'
22
+ * },
23
+ * },
24
+ * },
25
+ * })
26
+ * ```
27
+ */
28
+ export type PrinterZodMiniNodes = ast.PrinterPartial<string, PrinterZodMiniOptions>
29
+
30
+ export type PrinterZodMiniOptions = {
31
+ /**
32
+ * Use `z.guid()` or `z.uuid()` for UUID/GUID validation.
33
+ *
34
+ * @default 'uuid'
35
+ */
36
+ guidType?: PluginZod['resolvedOptions']['guidType']
37
+ /**
38
+ * Hook to transform generated Zod schema before output.
39
+ */
40
+ wrapOutput?: PluginZod['resolvedOptions']['wrapOutput']
41
+ /**
42
+ * Transforms raw schema names into valid JavaScript identifiers.
43
+ */
44
+ resolver?: ResolverZod
45
+ /**
46
+ * Properties to exclude using `.omit({ key: true })`.
47
+ */
48
+ keysToOmit?: Array<string>
49
+ /**
50
+ * Schema names that form circular dependency chains.
51
+ * Properties referencing these emit lazy getters wrapping refs in `z.lazy(() => …)`.
52
+ */
53
+ cyclicSchemas?: ReadonlySet<string>
54
+ /**
55
+ * Custom handler map for node type overrides.
56
+ */
57
+ nodes?: PrinterZodMiniNodes
58
+ }
59
+
60
+ /**
61
+ * Factory options for the Zod Mini printer, defining input/output types and configuration.
62
+ */
63
+ export type PrinterZodMiniFactory = ast.PrinterFactoryOptions<'zod-mini', PrinterZodMiniOptions, string, string>
64
+ /**
65
+ * Zod v4 **Mini** printer built with `definePrinter`.
66
+ *
67
+ * Converts a `SchemaNode` AST into a Zod v4 code string using the functional API
68
+ * (`z.optional(z.string())`) for improved tree-shaking. See {@link printerZod} for the chainable API.
69
+ *
70
+ * @example Functional Mini API
71
+ * ```ts
72
+ * const printer = printerZodMini({})
73
+ * const code = printer.print(optionalStringNode) // "z.optional(z.string())"
74
+ * ```
75
+ */
76
+ export const printerZodMini = ast.definePrinter<PrinterZodMiniFactory>((options) => {
77
+ return {
78
+ name: 'zod-mini',
79
+ options,
80
+ nodes: {
81
+ any: () => 'z.any()',
82
+ unknown: () => 'z.unknown()',
83
+ void: () => 'z.void()',
84
+ never: () => 'z.never()',
85
+ boolean: () => 'z.boolean()',
86
+ null: () => 'z.null()',
87
+ string(node) {
88
+ return `z.string()${lengthChecksMini(node)}`
89
+ },
90
+ number(node) {
91
+ return `z.number()${numberChecksMini(node)}`
92
+ },
93
+ integer(node) {
94
+ return `z.int()${numberChecksMini(node)}`
95
+ },
96
+ bigint(node) {
97
+ return `z.bigint()${numberChecksMini(node)}`
98
+ },
99
+ date(node) {
100
+ if (node.representation === 'string') {
101
+ return 'z.iso.date()'
102
+ }
103
+
104
+ return 'z.date()'
105
+ },
106
+ datetime() {
107
+ // Mini mode: datetime validation via z.string() (z.iso.datetime not available in mini)
108
+ return 'z.string()'
109
+ },
110
+ time(node) {
111
+ if (node.representation === 'string') {
112
+ return 'z.iso.time()'
113
+ }
114
+
115
+ return 'z.date()'
116
+ },
117
+ uuid(node) {
118
+ const base = this.options.guidType === 'guid' ? 'z.guid()' : 'z.uuid()'
119
+
120
+ return `${base}${lengthChecksMini(node)}`
121
+ },
122
+ email(node) {
123
+ return `z.email()${lengthChecksMini(node)}`
124
+ },
125
+ url(node) {
126
+ return `z.url()${lengthChecksMini(node)}`
127
+ },
128
+ ipv4: () => 'z.ipv4()',
129
+ ipv6: () => 'z.ipv6()',
130
+ blob: () => 'z.instanceof(File)',
131
+ enum(node) {
132
+ const values = node.namedEnumValues?.map((v) => v.value) ?? node.enumValues ?? []
133
+ const nonNullValues = values.filter((v): v is string | number | boolean => v !== null)
134
+
135
+ // asConst-style enum: use z.union([z.literal(…), …])
136
+ if (node.namedEnumValues?.length) {
137
+ const literals = nonNullValues.map((v) => `z.literal(${formatLiteral(v)})`)
138
+ if (literals.length === 1) return literals[0]!
139
+ return `z.union([${literals.join(', ')}])`
140
+ }
141
+
142
+ // Regular enum: use z.enum([…])
143
+ return `z.enum([${nonNullValues.map(formatLiteral).join(', ')}])`
144
+ },
145
+
146
+ ref(node) {
147
+ if (!node.name) return undefined
148
+ const refName = node.ref ? (ast.extractRefName(node.ref) ?? node.name) : node.name
149
+ const resolvedName = node.ref ? (this.options.resolver?.default(refName, 'function') ?? refName) : node.name
150
+
151
+ if (node.ref && this.options.cyclicSchemas?.has(refName)) {
152
+ return `z.lazy(() => ${resolvedName})`
153
+ }
154
+
155
+ return resolvedName
156
+ },
157
+ object(node) {
158
+ const properties = node.properties
159
+ .map((prop) => {
160
+ const { name: propName, schema } = prop
161
+
162
+ const meta = ast.syncSchemaRef(schema)
163
+
164
+ const isNullable = meta.nullable
165
+ const isOptional = schema.optional
166
+ const isNullish = schema.nullish
167
+
168
+ const hasSelfRef = this.options.cyclicSchemas != null && ast.containsCircularRef(schema, { circularSchemas: this.options.cyclicSchemas })
169
+ // Inside a getter the getter itself defers evaluation, so suppress
170
+ // z.lazy() wrapping on nested refs by temporarily clearing cyclicSchemas.
171
+ if (hasSelfRef) this.options.cyclicSchemas = undefined
172
+ const baseOutput = this.transform(schema) ?? this.transform(ast.createSchema({ type: 'unknown' }))!
173
+ if (hasSelfRef) this.options.cyclicSchemas = options.cyclicSchemas
174
+
175
+ const wrappedOutput = this.options.wrapOutput ? this.options.wrapOutput({ output: baseOutput, schema }) || baseOutput : baseOutput
176
+
177
+ const value = applyMiniModifiers({
178
+ value: wrappedOutput,
179
+ nullable: isNullable,
180
+ optional: isOptional,
181
+ nullish: isNullish,
182
+ defaultValue: meta.default,
183
+ })
184
+
185
+ if (hasSelfRef) {
186
+ return `get "${propName}"() { return ${value} }`
187
+ }
188
+ return `"${propName}": ${value}`
189
+ })
190
+ .join(',\n ')
191
+
192
+ return `z.object({\n ${properties}\n })`
193
+ },
194
+ array(node) {
195
+ const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
196
+ 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
+ }
202
+
203
+ return result
204
+ },
205
+ tuple(node) {
206
+ const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
207
+
208
+ return `z.tuple([${items.join(', ')}])`
209
+ },
210
+ union(node) {
211
+ const nodeMembers = node.members ?? []
212
+ const members = nodeMembers.map((m) => this.transform(m)).filter(Boolean)
213
+ if (members.length === 0) return ''
214
+ if (members.length === 1) return members[0]!
215
+ if (node.discriminatorPropertyName && !nodeMembers.some((m) => m.type === 'intersection')) {
216
+ // z.discriminatedUnion requires ZodObject members; intersections (ZodIntersection) are not
217
+ // assignable to $ZodDiscriminant, so fall back to z.union when any member is an intersection.
218
+ return `z.discriminatedUnion(${stringify(node.discriminatorPropertyName)}, [${members.join(', ')}])`
219
+ }
220
+
221
+ return `z.union([${members.join(', ')}])`
222
+ },
223
+ intersection(node) {
224
+ const members = node.members ?? []
225
+ if (members.length === 0) return ''
226
+
227
+ const [first, ...rest] = members
228
+ if (!first) return ''
229
+
230
+ let base = this.transform(first)
231
+ if (!base) return ''
232
+
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
+ }
256
+ const transformed = this.transform(member)
257
+ if (transformed) base = `z.intersection(${base}, ${transformed})`
258
+ }
259
+
260
+ return base
261
+ },
262
+ ...options.nodes,
263
+ },
264
+ print(node) {
265
+ const { keysToOmit } = this.options
266
+
267
+ let base = this.transform(node)
268
+ if (!base) return null
269
+
270
+ const meta = ast.syncSchemaRef(node)
271
+
272
+ if (keysToOmit?.length && meta.primitive === 'object' && !(meta.type === 'union' && meta.discriminatorPropertyName)) {
273
+ // Mirror printerTs `nonNullable: true`: when omitting keys, the resulting
274
+ // schema is a new non-nullable object type — skip optional/nullable/nullish.
275
+ // Discriminated unions (z.discriminatedUnion) do not support .omit(), so skip them.
276
+
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
+ }
285
+
286
+ return applyMiniModifiers({
287
+ value: base,
288
+ nullable: meta.nullable,
289
+ optional: meta.optional,
290
+ nullish: meta.nullish,
291
+ defaultValue: meta.default,
292
+ })
293
+ },
294
+ }
295
+ })
@@ -0,0 +1,57 @@
1
+ import { camelCase, pascalCase } from '@internals/utils'
2
+ import { defineResolver } from '@kubb/core'
3
+ import type { PluginZod } from '../types.ts'
4
+
5
+ /**
6
+ * Naming convention resolver for Zod plugin.
7
+ *
8
+ * Provides default naming helpers using camelCase with a `Schema` suffix for schemas.
9
+ *
10
+ * @example
11
+ * `resolverZod.default('list pets', 'function') // → 'listPetsSchema'`
12
+ */
13
+ export const resolverZod = defineResolver<PluginZod>((ctx) => {
14
+ return {
15
+ name: 'default',
16
+ pluginName: 'plugin-zod',
17
+ default(name, type) {
18
+ return camelCase(name, { isFile: type === 'file', suffix: type ? 'schema' : undefined })
19
+ },
20
+ resolveSchemaName(name) {
21
+ return camelCase(name, { suffix: 'schema' })
22
+ },
23
+ resolveSchemaTypeName(name) {
24
+ return pascalCase(name, { suffix: 'schema' })
25
+ },
26
+ resolveTypeName(name) {
27
+ return pascalCase(name)
28
+ },
29
+ resolvePathName(name, type) {
30
+ return ctx.default(name, type)
31
+ },
32
+ resolveParamName(node, param) {
33
+ return ctx.resolveSchemaName(`${node.operationId} ${param.in} ${param.name}`)
34
+ },
35
+ resolveResponseStatusName(node, statusCode) {
36
+ return ctx.resolveSchemaName(`${node.operationId} Status ${statusCode}`)
37
+ },
38
+ resolveDataName(node) {
39
+ return ctx.resolveSchemaName(`${node.operationId} Data`)
40
+ },
41
+ resolveResponsesName(node) {
42
+ return ctx.resolveSchemaName(`${node.operationId} Responses`)
43
+ },
44
+ resolveResponseName(node) {
45
+ return ctx.resolveSchemaName(`${node.operationId} Response`)
46
+ },
47
+ resolvePathParamsName(node, param) {
48
+ return ctx.resolveParamName(node, param)
49
+ },
50
+ resolveQueryParamsName(node, param) {
51
+ return ctx.resolveParamName(node, param)
52
+ },
53
+ resolveHeaderParamsName(node, param) {
54
+ return ctx.resolveParamName(node, param)
55
+ },
56
+ }
57
+ })