@kubb/plugin-zod 5.0.0-beta.10 → 5.0.0-beta.103

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.
@@ -1,216 +0,0 @@
1
- import type { Adapter } from '@kubb/core'
2
- import { ast, defineGenerator } from '@kubb/core'
3
- import type { AdapterOas } from '@kubb/adapter-oas'
4
- import { File, jsxRenderer } from '@kubb/renderer-jsx'
5
- import { Operations } from '../components/Operations.tsx'
6
- import { Zod } from '../components/Zod.tsx'
7
- import { ZOD_NAMESPACE_IMPORTS } from '../constants.ts'
8
- import { printerZod } from '../printers/printerZod.ts'
9
- import { printerZodMini } from '../printers/printerZodMini.ts'
10
- import type { PluginZod } from '../types'
11
- import { buildSchemaNames } from '../utils.ts'
12
-
13
- export const zodGenerator = defineGenerator<PluginZod>({
14
- name: 'zod',
15
- renderer: jsxRenderer,
16
- schema(node, ctx) {
17
- const { adapter, config, resolver, root } = ctx
18
- const { output, coercion, guidType, mini, wrapOutput, inferred, importPath, group, printer } = ctx.options
19
- const dateType = (adapter as Adapter<AdapterOas>).options.dateType
20
-
21
- if (!node.name) {
22
- return
23
- }
24
-
25
- const mode = ctx.getMode(output)
26
- const isZodImport = ZOD_NAMESPACE_IMPORTS.has(importPath as 'zod' | 'zod/mini')
27
-
28
- const imports = adapter.getImports(node, (schemaName) => ({
29
- name: resolver.resolveSchemaName(schemaName),
30
- path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }).path,
31
- }))
32
-
33
- const meta = {
34
- name: resolver.resolveSchemaName(node.name),
35
- file: resolver.resolveFile({ name: node.name, extname: '.ts' }, { root, output, group }),
36
- } as const
37
-
38
- const inferTypeName = inferred ? resolver.resolveSchemaTypeName(node.name) : undefined
39
-
40
- const cyclicSchemas = adapter.inputNode ? ast.findCircularSchemas(adapter.inputNode.schemas) : undefined
41
-
42
- const schemaPrinter = mini
43
- ? printerZodMini({ guidType, wrapOutput, resolver, cyclicSchemas, nodes: printer?.nodes })
44
- : printerZod({ coercion, guidType, dateType, wrapOutput, resolver, cyclicSchemas, nodes: printer?.nodes })
45
-
46
- return (
47
- <File
48
- baseName={meta.file.baseName}
49
- path={meta.file.path}
50
- meta={meta.file.meta}
51
- banner={resolver.resolveBanner(adapter.inputNode, { output, config })}
52
- footer={resolver.resolveFooter(adapter.inputNode, { output, config })}
53
- >
54
- <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} />)}
56
-
57
- <Zod name={meta.name} node={node} printer={schemaPrinter} inferTypeName={inferTypeName} />
58
- </File>
59
- )
60
- },
61
- operation(node, ctx) {
62
- const { adapter, config, resolver, root } = ctx
63
- const { output, coercion, guidType, mini, wrapOutput, inferred, importPath, group, paramsCasing, printer } = ctx.options
64
- const dateType = (adapter as Adapter<AdapterOas>).options.dateType
65
-
66
- const mode = ctx.getMode(output)
67
- const isZodImport = ZOD_NAMESPACE_IMPORTS.has(importPath as 'zod' | 'zod/mini')
68
-
69
- const params = ast.caseParams(node.parameters, paramsCasing)
70
-
71
- const meta = {
72
- file: resolver.resolveFile({ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path }, { root, output, group }),
73
- } as const
74
-
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> }) {
78
- if (!schema) return null
79
-
80
- const inferTypeName = inferred ? resolver.resolveTypeName(name) : undefined
81
-
82
- const imports = adapter.getImports(schema, (schemaName) => ({
83
- name: resolver.resolveSchemaName(schemaName),
84
- path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }).path,
85
- }))
86
-
87
- 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 })
90
-
91
- return (
92
- <>
93
- {mode === 'split' &&
94
- imports.map((imp) => <File.Import key={[name, imp.path, imp.name].join('-')} root={meta.file.path} path={imp.path} name={imp.name} />)}
95
- <Zod name={name} node={schema} printer={schemaPrinter} inferTypeName={inferTypeName} />
96
- </>
97
- )
98
- }
99
-
100
- const paramSchemas = params.map((param) => renderSchemaEntry({ schema: param.schema, name: resolver.resolveParamName(node, param) }))
101
-
102
- const responseSchemas = node.responses.map((res) =>
103
- renderSchemaEntry({
104
- schema: res.schema,
105
- name: resolver.resolveResponseStatusName(node, res.statusCode),
106
- keysToOmit: res.keysToOmit,
107
- }),
108
- )
109
-
110
- const responsesWithSchema = node.responses.filter((res) => res.schema)
111
- const responseUnionSchema =
112
- responsesWithSchema.length > 0
113
- ? (() => {
114
- const responseUnionName = resolver.resolveResponseName(node)
115
-
116
- // Collect all import names from response schemas to detect naming collisions.
117
- // When a response is a $ref to a component schema whose resolved name matches
118
- // the response union name, skip generation to avoid redeclaration errors.
119
- const importedNames = new Set(
120
- 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
- : [],
129
- ),
130
- )
131
-
132
- if (importedNames.has(responseUnionName)) {
133
- return null
134
- }
135
-
136
- const members = responsesWithSchema.map((res) => ast.createSchema({ type: 'ref', name: resolver.resolveResponseStatusName(node, res.statusCode) }))
137
- const unionNode = members.length === 1 ? members[0]! : ast.createSchema({ type: 'union', members })
138
-
139
- return renderSchemaEntry({
140
- schema: unionNode,
141
- name: responseUnionName,
142
- })
143
- })()
144
- : null
145
-
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
- },
152
- name: resolver.resolveDataName(node),
153
- keysToOmit: node.requestBody.content![0]!.keysToOmit,
154
- })
155
- : null
156
-
157
- return (
158
- <File
159
- baseName={meta.file.baseName}
160
- path={meta.file.path}
161
- meta={meta.file.meta}
162
- banner={resolver.resolveBanner(adapter.inputNode, { output, config })}
163
- footer={resolver.resolveFooter(adapter.inputNode, { output, config })}
164
- >
165
- <File.Import name={isZodImport ? 'z' : ['z']} path={importPath} isNameSpace={isZodImport} />
166
- {paramSchemas}
167
- {responseSchemas}
168
- {responseUnionSchema}
169
- {requestSchema}
170
- </File>
171
- )
172
- },
173
- operations(nodes, ctx) {
174
- const { adapter, config, resolver, root } = ctx
175
- const { output, importPath, group, operations, paramsCasing } = ctx.options
176
-
177
- if (!operations) {
178
- return
179
- }
180
- const isZodImport = ZOD_NAMESPACE_IMPORTS.has(importPath as 'zod' | 'zod/mini')
181
-
182
- const meta = {
183
- file: resolver.resolveFile({ name: 'operations', extname: '.ts' }, { root, output, group }),
184
- } as const
185
-
186
- const transformedOperations = nodes.map((node) => {
187
- const params = ast.caseParams(node.parameters, paramsCasing)
188
-
189
- return {
190
- node,
191
- data: buildSchemaNames(node, { params, resolver }),
192
- }
193
- })
194
-
195
- 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 })
198
-
199
- return names.map((name) => <File.Import key={[name, opFile.path].join('-')} name={[name]} root={meta.file.path} path={opFile.path} />)
200
- })
201
-
202
- return (
203
- <File
204
- baseName={meta.file.baseName}
205
- path={meta.file.path}
206
- meta={meta.file.meta}
207
- banner={resolver.resolveBanner(adapter.inputNode, { output, config })}
208
- footer={resolver.resolveFooter(adapter.inputNode, { output, config })}
209
- >
210
- <File.Import isTypeOnly name={isZodImport ? 'z' : ['z']} path={importPath} isNameSpace={isZodImport} />
211
- {imports}
212
- <Operations name="operations" operations={transformedOperations} />
213
- </File>
214
- )
215
- },
216
- })
package/src/index.ts DELETED
@@ -1,11 +0,0 @@
1
- export { zodGenerator } from './generators/zodGenerator.tsx'
2
-
3
- export { default, pluginZod, pluginZodName } from './plugin.ts'
4
- export type { PrinterZodFactory, PrinterZodNodes, PrinterZodOptions } from './printers/printerZod.ts'
5
- export { printerZod } from './printers/printerZod.ts'
6
- export type { PrinterZodMiniFactory, PrinterZodMiniNodes, PrinterZodMiniOptions } from './printers/printerZodMini.ts'
7
- export { printerZodMini } from './printers/printerZodMini.ts'
8
-
9
- export { resolverZod } from './resolvers/resolverZod.ts'
10
-
11
- export type { PluginZod, ResolverZod } from './types.ts'
package/src/plugin.ts DELETED
@@ -1,94 +0,0 @@
1
- import { camelCase } from '@internals/utils'
2
- import { definePlugin, type Group } from '@kubb/core'
3
- import { zodGenerator } from './generators/zodGenerator.tsx'
4
- import { resolverZod } from './resolvers/resolverZod.ts'
5
- import type { PluginZod } from './types.ts'
6
-
7
- /**
8
- * Canonical plugin name for `@kubb/plugin-zod`, used in driver lookups and warnings.
9
- */
10
- export const pluginZodName = 'plugin-zod' satisfies PluginZod['name']
11
-
12
- /**
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`.
16
- *
17
- * @example Zod schema generator
18
- * ```ts
19
- * import pluginZod from '@kubb/plugin-zod'
20
- * export default defineConfig({
21
- * plugins: [pluginZod({ output: { path: 'zod' } })]
22
- * })
23
- * ```
24
- */
25
- export const pluginZod = definePlugin<PluginZod>((options) => {
26
- const {
27
- output = { path: 'zod', barrelType: 'named' },
28
- group,
29
- exclude = [],
30
- include,
31
- override = [],
32
- typed = false,
33
- operations = false,
34
- mini = false,
35
- guidType = 'uuid',
36
- importPath = mini ? 'zod/mini' : 'zod',
37
- coercion = false,
38
- inferred = false,
39
- wrapOutput = undefined,
40
- paramsCasing,
41
- printer,
42
- resolver: userResolver,
43
- transformer: userTransformer,
44
- generators: userGenerators = [],
45
- } = options
46
-
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
58
-
59
- return {
60
- name: pluginZodName,
61
- options,
62
- hooks: {
63
- 'kubb:plugin:setup'(ctx) {
64
- ctx.setOptions({
65
- output,
66
- exclude,
67
- include,
68
- override,
69
- group: groupConfig,
70
- typed,
71
- importPath,
72
- coercion,
73
- operations,
74
- inferred,
75
- guidType,
76
- mini,
77
- wrapOutput,
78
- paramsCasing,
79
- printer,
80
- })
81
- ctx.setResolver(userResolver ? { ...resolverZod, ...userResolver } : resolverZod)
82
- if (userTransformer) {
83
- ctx.setTransformer(userTransformer)
84
- }
85
- ctx.addGenerator(zodGenerator)
86
- for (const gen of userGenerators) {
87
- ctx.addGenerator(gen)
88
- }
89
- },
90
- },
91
- }
92
- })
93
-
94
- export default pluginZod
@@ -1,365 +0,0 @@
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
- function strictOneOfMember(member: string, node: ast.SchemaNode): string {
74
- if (node.type === 'object' && node.additionalProperties === undefined) {
75
- return `${member}.strict()`
76
- }
77
-
78
- if (node.type === 'ref') {
79
- if (member.startsWith('z.lazy(')) {
80
- return member
81
- }
82
-
83
- const schema = ast.syncSchemaRef(node)
84
-
85
- if (schema.type === 'object' && (schema.additionalProperties === undefined || schema.additionalProperties === false)) {
86
- return `${member}.strict()`
87
- }
88
- }
89
-
90
- return member
91
- }
92
-
93
- /**
94
- * Zod v4 printer built with `definePrinter`.
95
- *
96
- * Converts a `SchemaNode` AST into a Zod v4 code string using the chainable API
97
- * (`.optional()`, `.nullable()`, `.omit()`, etc.). For improved tree-shaking, see {@link printerZodMini}.
98
- *
99
- * @example Chainable API
100
- * ```ts
101
- * const printer = printerZod({ coercion: false })
102
- * const code = printer.print(stringNode) // "z.string()"
103
- * ```
104
- */
105
- export const printerZod = ast.definePrinter<PrinterZodFactory>((options) => {
106
- return {
107
- name: 'zod',
108
- options,
109
- nodes: {
110
- any: () => 'z.any()',
111
- unknown: () => 'z.unknown()',
112
- void: () => 'z.void()',
113
- never: () => 'z.never()',
114
- boolean: () => 'z.boolean()',
115
- null: () => 'z.null()',
116
- string(node) {
117
- const base = shouldCoerce(this.options.coercion, 'strings') ? 'z.coerce.string()' : 'z.string()'
118
-
119
- return `${base}${lengthConstraints(node)}`
120
- },
121
- number(node) {
122
- const base = shouldCoerce(this.options.coercion, 'numbers') ? 'z.coerce.number()' : 'z.number()'
123
-
124
- return `${base}${numberConstraints(node)}`
125
- },
126
- integer(node) {
127
- const base = shouldCoerce(this.options.coercion, 'numbers') ? 'z.coerce.number().int()' : 'z.int()'
128
-
129
- return `${base}${numberConstraints(node)}`
130
- },
131
- bigint() {
132
- return shouldCoerce(this.options.coercion, 'numbers') ? 'z.coerce.bigint()' : 'z.bigint()'
133
- },
134
- date(node) {
135
- if (node.representation === 'string') {
136
- return 'z.iso.date()'
137
- }
138
-
139
- return shouldCoerce(this.options.coercion, 'dates') ? 'z.coerce.date()' : 'z.date()'
140
- },
141
- datetime(node) {
142
- const offset = node.offset || this.options.dateType === 'stringOffset'
143
- const local = node.local || this.options.dateType === 'stringLocal'
144
-
145
- if (offset) return 'z.iso.datetime({ offset: true })'
146
- if (local) return 'z.iso.datetime({ local: true })'
147
-
148
- return 'z.iso.datetime()'
149
- },
150
- time(node) {
151
- if (node.representation === 'string') {
152
- return 'z.iso.time()'
153
- }
154
-
155
- return shouldCoerce(this.options.coercion, 'dates') ? 'z.coerce.date()' : 'z.date()'
156
- },
157
- uuid(node) {
158
- const base = this.options.guidType === 'guid' ? 'z.guid()' : 'z.uuid()'
159
-
160
- return `${base}${lengthConstraints(node)}`
161
- },
162
- email(node) {
163
- return `z.email()${lengthConstraints(node)}`
164
- },
165
- url(node) {
166
- return `z.url()${lengthConstraints(node)}`
167
- },
168
- ipv4: () => 'z.ipv4()',
169
- ipv6: () => 'z.ipv6()',
170
- blob: () => 'z.instanceof(File)',
171
- enum(node) {
172
- const values = node.namedEnumValues?.map((v) => v.value) ?? node.enumValues ?? []
173
- const nonNullValues = values.filter((v): v is string | number | boolean => v !== null)
174
-
175
- // asConst-style enum: use z.union([z.literal(…), …])
176
- if (node.namedEnumValues?.length) {
177
- const literals = nonNullValues.map((v) => `z.literal(${formatLiteral(v)})`)
178
-
179
- if (literals.length === 1) return literals[0]!
180
- return `z.union([${literals.join(', ')}])`
181
- }
182
-
183
- // Regular enum: use z.enum([…])
184
- return `z.enum([${nonNullValues.map(formatLiteral).join(', ')}])`
185
- },
186
- ref(node) {
187
- if (!node.name) return undefined
188
- const refName = node.ref ? (ast.extractRefName(node.ref) ?? node.name) : node.name
189
- const resolvedName = node.ref ? (this.options.resolver?.default(refName, 'function') ?? refName) : node.name
190
-
191
- if (node.ref && this.options.cyclicSchemas?.has(refName)) {
192
- return `z.lazy(() => ${resolvedName})`
193
- }
194
-
195
- return resolvedName
196
- },
197
- object(node) {
198
- const properties = node.properties
199
- .map((prop) => {
200
- const { name: propName, schema } = prop
201
-
202
- const meta = ast.syncSchemaRef(schema)
203
-
204
- const isNullable = meta.nullable
205
- const isOptional = schema.optional
206
- const isNullish = schema.nullish
207
-
208
- const hasSelfRef = this.options.cyclicSchemas != null && ast.containsCircularRef(schema, { circularSchemas: this.options.cyclicSchemas })
209
- // Inside a getter the getter itself defers evaluation, so suppress
210
- // z.lazy() wrapping on nested refs by temporarily clearing cyclicSchemas.
211
- if (hasSelfRef) this.options.cyclicSchemas = undefined
212
- const baseOutput = this.transform(schema) ?? this.transform(ast.createSchema({ type: 'unknown' }))!
213
- if (hasSelfRef) this.options.cyclicSchemas = options.cyclicSchemas
214
-
215
- const wrappedOutput = this.options.wrapOutput ? this.options.wrapOutput({ output: baseOutput, schema }) || baseOutput : baseOutput
216
-
217
- // When a property schema is not a ref but the metadata is from a ref (e.g., discriminator
218
- // property override), skip applying the description from the ref target to avoid applying
219
- // metadata from a replaced schema.
220
- let descriptionToApply = meta.description
221
- if (schema.type !== 'ref' && meta.type === 'ref') {
222
- descriptionToApply = undefined
223
- }
224
-
225
- const value = applyModifiers({
226
- value: wrappedOutput,
227
- nullable: isNullable,
228
- optional: isOptional,
229
- nullish: isNullish,
230
- defaultValue: meta.default,
231
- description: descriptionToApply,
232
- })
233
-
234
- if (hasSelfRef) {
235
- return `get "${propName}"() { return ${value} }`
236
- }
237
- return `"${propName}": ${value}`
238
- })
239
- .join(',\n ')
240
-
241
- let result = `z.object({\n ${properties}\n })`
242
-
243
- // Handle additionalProperties as .catchall() or .strict()
244
- if (node.additionalProperties && node.additionalProperties !== true) {
245
- const catchallType = this.transform(node.additionalProperties)
246
- if (catchallType) {
247
- result += `.catchall(${catchallType})`
248
- }
249
- } else if (node.additionalProperties === true) {
250
- result += `.catchall(${this.transform(ast.createSchema({ type: 'unknown' }))})`
251
- } else if (node.additionalProperties === false) {
252
- result += '.strict()'
253
- }
254
-
255
- return result
256
- },
257
- array(node) {
258
- const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
259
- const inner = items.join(', ') || this.transform(ast.createSchema({ type: 'unknown' }))!
260
- let result = `z.array(${inner})${lengthConstraints(node)}`
261
-
262
- if (node.unique) {
263
- result += `.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })`
264
- }
265
-
266
- return result
267
- },
268
- tuple(node) {
269
- const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
270
-
271
- return `z.tuple([${items.join(', ')}])`
272
- },
273
- union(node) {
274
- const nodeMembers = node.members ?? []
275
- const members = nodeMembers
276
- .map((memberNode) => {
277
- const member = this.transform(memberNode)
278
-
279
- return member && node.strategy === 'one' ? strictOneOfMember(member, memberNode) : member
280
- })
281
- .filter(Boolean)
282
- if (members.length === 0) return ''
283
- if (members.length === 1) return members[0]!
284
- if (node.discriminatorPropertyName && !nodeMembers.some((m) => m.type === 'intersection')) {
285
- // z.discriminatedUnion requires ZodObject members; intersections (ZodIntersection) are not
286
- // assignable to $ZodDiscriminant, so fall back to z.union when any member is an intersection.
287
- return `z.discriminatedUnion(${stringify(node.discriminatorPropertyName)}, [${members.join(', ')}])`
288
- }
289
-
290
- return `z.union([${members.join(', ')}])`
291
- },
292
- intersection(node) {
293
- const members = node.members ?? []
294
- if (members.length === 0) return ''
295
-
296
- const [first, ...rest] = members
297
- if (!first) return ''
298
-
299
- let base = this.transform(first)
300
- if (!base) return ''
301
-
302
- for (const member of rest) {
303
- if (member.primitive === 'string') {
304
- const s = ast.narrowSchema(member, 'string')
305
- const c = lengthConstraints(s ?? {})
306
- if (c) {
307
- base += c
308
- continue
309
- }
310
- } else if (member.primitive === 'number' || member.primitive === 'integer') {
311
- const n = ast.narrowSchema(member, 'number') ?? ast.narrowSchema(member, 'integer')
312
- const c = numberConstraints(n ?? {})
313
- if (c) {
314
- base += c
315
- continue
316
- }
317
- } else if (member.primitive === 'array') {
318
- const a = ast.narrowSchema(member, 'array')
319
- const c = lengthConstraints(a ?? {})
320
- if (c) {
321
- base += c
322
- continue
323
- }
324
- }
325
- const transformed = this.transform(member)
326
- if (transformed) base = `${base}.and(${transformed})`
327
- }
328
-
329
- return base
330
- },
331
- ...options.nodes,
332
- },
333
- print(node) {
334
- const { keysToOmit } = this.options
335
-
336
- let base = this.transform(node)
337
- if (!base) return null
338
-
339
- const meta = ast.syncSchemaRef(node)
340
-
341
- if (keysToOmit?.length && meta.primitive === 'object' && !(meta.type === 'union' && meta.discriminatorPropertyName)) {
342
- // Mirror printerTs `nonNullable: true`: when omitting keys, the resulting
343
- // schema is a new non-nullable object type — skip optional/nullable/nullish.
344
- // Discriminated unions (z.discriminatedUnion) do not support .omit(), so skip them.
345
-
346
- // If this is a lazy reference, apply omit inside the lazy function
347
- const lazyMatch = base.match(/^z\.lazy\(\(\)\s*=>\s*(.+)\)$/)
348
- if (lazyMatch) {
349
- base = `z.lazy(() => ${lazyMatch[1]}.omit({ ${keysToOmit.map((k: string) => `"${k}": true`).join(', ')} }))`
350
- } else {
351
- base = `${base}.omit({ ${keysToOmit.map((k: string) => `"${k}": true`).join(', ')} })`
352
- }
353
- }
354
-
355
- return applyModifiers({
356
- value: base,
357
- nullable: meta.nullable,
358
- optional: meta.optional,
359
- nullish: meta.nullish,
360
- defaultValue: meta.default,
361
- description: meta.description,
362
- })
363
- },
364
- }
365
- })