@kubb/plugin-zod 5.0.0-beta.4 → 5.0.0-beta.56

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,8 +1,8 @@
1
- import { stringify } from '@internals/utils'
1
+ import { buildList, buildObject, extractRefName, objectKey, stringify } from '@kubb/ast/utils'
2
2
 
3
3
  import { ast } from '@kubb/core'
4
4
  import type { PluginZod, ResolverZod } from '../types.ts'
5
- import { applyModifiers, formatLiteral, lengthConstraints, numberConstraints, shouldCoerce } from '../utils.ts'
5
+ import { applyModifiers, containsCodec, formatLiteral, getCodec, lengthConstraints, numberConstraints, shouldCoerce } from '../utils.ts'
6
6
  import type { AdapterOas } from '@kubb/adapter-oas'
7
7
 
8
8
  /**
@@ -53,12 +53,21 @@ 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(() => …)`.
60
60
  */
61
61
  cyclicSchemas?: ReadonlySet<string>
62
+ /**
63
+ * Print direction for `dateType: 'date'` fields (`Date` in TypeScript):
64
+ * - `'output'` (default) — decode the wire `string` into a `Date` (response bodies).
65
+ * - `'input'` — encode a `Date` back into the wire `string` (request bodies/params).
66
+ *
67
+ * Diverging the directions requires the generator to emit an `${name}InputSchema`
68
+ * variant for each date-bearing component.
69
+ */
70
+ direction?: 'input' | 'output'
62
71
  /**
63
72
  * Custom handler map for node type overrides.
64
73
  */
@@ -70,6 +79,33 @@ export type PrinterZodOptions = {
70
79
  */
71
80
  export type PrinterZodFactory = ast.PrinterFactoryOptions<'zod', PrinterZodOptions, string, string>
72
81
 
82
+ function strictOneOfMember(member: string, node: ast.SchemaNode): string {
83
+ if (node.type === 'object' && node.additionalProperties === undefined) {
84
+ return `${member}.strict()`
85
+ }
86
+
87
+ if (node.type === 'ref') {
88
+ if (member.startsWith('z.lazy(')) {
89
+ return member
90
+ }
91
+
92
+ const schema = ast.syncSchemaRef(node)
93
+
94
+ if (schema.type === 'object' && (schema.additionalProperties === undefined || schema.additionalProperties === false)) {
95
+ return `${member}.strict()`
96
+ }
97
+ }
98
+
99
+ return member
100
+ }
101
+
102
+ function getMemberConstraint(member: ast.SchemaNode): string | undefined {
103
+ if (member.primitive === 'string') return lengthConstraints(ast.narrowSchema(member, 'string') ?? {}) || undefined
104
+ if (member.primitive === 'number' || member.primitive === 'integer')
105
+ return numberConstraints(ast.narrowSchema(member, 'number') ?? ast.narrowSchema(member, 'integer') ?? {}) || undefined
106
+ if (member.primitive === 'array') return lengthConstraints(ast.narrowSchema(member, 'array') ?? {}) || undefined
107
+ }
108
+
73
109
  /**
74
110
  * Zod v4 printer built with `definePrinter`.
75
111
  *
@@ -112,11 +148,13 @@ export const printerZod = ast.definePrinter<PrinterZodFactory>((options) => {
112
148
  return shouldCoerce(this.options.coercion, 'numbers') ? 'z.coerce.bigint()' : 'z.bigint()'
113
149
  },
114
150
  date(node) {
115
- if (node.representation === 'string') {
116
- return 'z.iso.date()'
151
+ // representation: 'date' → typed as `Date`; decode/encode at the boundary.
152
+ const codec = getCodec(node)
153
+ if (codec) {
154
+ return this.options.direction === 'input' ? codec.encode(node) : codec.decode(node)
117
155
  }
118
156
 
119
- return shouldCoerce(this.options.coercion, 'dates') ? 'z.coerce.date()' : 'z.date()'
157
+ return 'z.iso.date()'
120
158
  },
121
159
  datetime(node) {
122
160
  const offset = node.offset || this.options.dateType === 'stringOffset'
@@ -164,9 +202,17 @@ export const printerZod = ast.definePrinter<PrinterZodFactory>((options) => {
164
202
  return `z.enum([${nonNullValues.map(formatLiteral).join(', ')}])`
165
203
  },
166
204
  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
205
+ if (!node.name) return null
206
+ const refName = node.ref ? (extractRefName(node.ref) ?? node.name) : node.name
207
+
208
+ // In the input direction, a date-bearing component resolves to its `${name}InputSchema`
209
+ // variant so request bodies encode `Date → string` instead of decoding.
210
+ const useInputVariant = node.ref != null && this.options.direction === 'input' && containsCodec(node)
211
+ const resolvedName = node.ref
212
+ ? useInputVariant
213
+ ? (this.options.resolver?.resolveInputSchemaName(refName) ?? refName)
214
+ : (this.options.resolver?.default(refName, 'function') ?? refName)
215
+ : node.name
170
216
 
171
217
  if (node.ref && this.options.cyclicSchemas?.has(refName)) {
172
218
  return `z.lazy(() => ${resolvedName})`
@@ -175,93 +221,92 @@ export const printerZod = ast.definePrinter<PrinterZodFactory>((options) => {
175
221
  return resolvedName
176
222
  },
177
223
  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}`
224
+ const entries = node.properties.map((prop) => {
225
+ const { name: propName, schema } = prop
226
+
227
+ const meta = ast.syncSchemaRef(schema)
228
+
229
+ const isNullable = meta.nullable
230
+ const isOptional = schema.optional
231
+ const isNullish = schema.nullish
232
+
233
+ const hasSelfRef = this.options.cyclicSchemas != null && ast.containsCircularRef(schema, { circularSchemas: this.options.cyclicSchemas })
234
+ // Inside a getter the getter itself defers evaluation, so suppress
235
+ // z.lazy() wrapping on nested refs by temporarily clearing cyclicSchemas.
236
+ // Save before clearing: this.options === options (same reference via definePrinter),
237
+ // so reading options.cyclicSchemas after mutation would return undefined.
238
+ const savedCyclicSchemas = this.options.cyclicSchemas
239
+ if (hasSelfRef) this.options.cyclicSchemas = undefined
240
+ const baseOutput = this.transform(schema) ?? this.transform(ast.createSchema({ type: 'unknown' }))!
241
+ if (hasSelfRef) this.options.cyclicSchemas = savedCyclicSchemas
242
+
243
+ const wrappedOutput = this.options.wrapOutput ? this.options.wrapOutput({ output: baseOutput, schema }) || baseOutput : baseOutput
244
+
245
+ // When a property schema is not a ref but the metadata is from a ref (e.g., discriminator
246
+ // property override), skip applying the description from the ref target to avoid applying
247
+ // metadata from a replaced schema.
248
+ const descriptionToApply = schema.type !== 'ref' && meta.type === 'ref' ? undefined : meta.description
249
+
250
+ const value = applyModifiers({
251
+ value: wrappedOutput,
252
+ nullable: isNullable,
253
+ optional: isOptional,
254
+ nullish: isNullish,
255
+ defaultValue: meta.default,
256
+ description: descriptionToApply,
218
257
  })
219
- .join(',\n ')
220
258
 
221
- let result = `z.object({\n ${properties}\n })`
259
+ if (hasSelfRef) {
260
+ return `get ${objectKey(propName)}() { return ${value} }`
261
+ }
262
+ return `${objectKey(propName)}: ${value}`
263
+ })
264
+
265
+ const objectBase = `z.object(${buildObject(entries)})`
222
266
 
223
267
  // 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})`
268
+ const result = (() => {
269
+ if (node.additionalProperties && node.additionalProperties !== true) {
270
+ const catchallType = this.transform(node.additionalProperties)
271
+ return catchallType ? `${objectBase}.catchall(${catchallType})` : objectBase
228
272
  }
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
- }
273
+ if (node.additionalProperties === true) return `${objectBase}.catchall(${this.transform(ast.createSchema({ type: 'unknown' }))})`
274
+ if (node.additionalProperties === false) return `${objectBase}.strict()`
275
+ return objectBase
276
+ })()
234
277
 
235
278
  return result
236
279
  },
237
280
  array(node) {
238
281
  const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
239
282
  const inner = items.join(', ') || this.transform(ast.createSchema({ type: 'unknown' }))!
240
- let result = `z.array(${inner})${lengthConstraints(node)}`
283
+ const base = `z.array(${inner})${lengthConstraints(node)}`
241
284
 
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
285
+ return node.unique ? `${base}.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })` : base
247
286
  },
248
287
  tuple(node) {
249
288
  const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
250
289
 
251
- return `z.tuple([${items.join(', ')}])`
290
+ return `z.tuple(${buildList(items)})`
252
291
  },
253
292
  union(node) {
254
293
  const nodeMembers = node.members ?? []
255
- const members = nodeMembers.map((m) => this.transform(m)).filter(Boolean)
294
+ const members = nodeMembers
295
+ .map((memberNode) => {
296
+ const member = this.transform(memberNode)
297
+
298
+ return member && node.strategy === 'one' ? strictOneOfMember(member, memberNode) : member
299
+ })
300
+ .filter(Boolean)
256
301
  if (members.length === 0) return ''
257
302
  if (members.length === 1) return members[0]!
258
303
  if (node.discriminatorPropertyName && !nodeMembers.some((m) => m.type === 'intersection')) {
259
304
  // z.discriminatedUnion requires ZodObject members; intersections (ZodIntersection) are not
260
305
  // assignable to $ZodDiscriminant, so fall back to z.union when any member is an intersection.
261
- return `z.discriminatedUnion(${stringify(node.discriminatorPropertyName)}, [${members.join(', ')}])`
306
+ return `z.discriminatedUnion(${stringify(node.discriminatorPropertyName)}, ${buildList(members)})`
262
307
  }
263
308
 
264
- return `z.union([${members.join(', ')}])`
309
+ return `z.union(${buildList(members)})`
265
310
  },
266
311
  intersection(node) {
267
312
  const members = node.members ?? []
@@ -270,61 +315,37 @@ export const printerZod = ast.definePrinter<PrinterZodFactory>((options) => {
270
315
  const [first, ...rest] = members
271
316
  if (!first) return ''
272
317
 
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
- }
318
+ const firstBase = this.transform(first)
319
+ if (!firstBase) return ''
302
320
 
303
- return base
321
+ return rest.reduce((acc, member) => {
322
+ const constraint = getMemberConstraint(member)
323
+ if (constraint) return acc + constraint
324
+ const transformed = this.transform(member)
325
+ return transformed ? `${acc}.and(${transformed})` : acc
326
+ }, firstBase)
304
327
  },
305
328
  ...options.nodes,
306
329
  },
307
330
  print(node) {
308
331
  const { keysToOmit } = this.options
309
332
 
310
- let base = this.transform(node)
311
- if (!base) return null
333
+ const transformed = this.transform(node)
334
+ if (!transformed) return null
312
335
 
313
336
  const meta = ast.syncSchemaRef(node)
314
337
 
315
- if (keysToOmit?.length && meta.primitive === 'object' && !(meta.type === 'union' && meta.discriminatorPropertyName)) {
338
+ const base = (() => {
339
+ if (!keysToOmit?.length || meta.primitive !== 'object' || (meta.type === 'union' && meta.discriminatorPropertyName)) return transformed
316
340
  // Mirror printerTs `nonNullable: true`: when omitting keys, the resulting
317
341
  // schema is a new non-nullable object type — skip optional/nullable/nullish.
318
342
  // Discriminated unions (z.discriminatedUnion) do not support .omit(), so skip them.
319
343
 
320
344
  // 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
- }
345
+ const lazyMatch = transformed.match(/^z\.lazy\(\(\)\s*=>\s*(.+)\)$/)
346
+ if (lazyMatch) return `z.lazy(() => ${lazyMatch[1]}.omit({ ${keysToOmit.map((k: string) => `"${k}": true`).join(', ')} }))`
347
+ return `${transformed}.omit({ ${keysToOmit.map((k: string) => `"${k}": true`).join(', ')} })`
348
+ })()
328
349
 
329
350
  return applyModifiers({
330
351
  value: base,
@@ -1,4 +1,4 @@
1
- import { stringify } from '@internals/utils'
1
+ import { buildList, buildObject, extractRefName, objectKey, stringify } from '@kubb/ast/utils'
2
2
 
3
3
  import { ast } from '@kubb/core'
4
4
  import type { PluginZod, ResolverZod } from '../types.ts'
@@ -45,7 +45,7 @@ export type PrinterZodMiniOptions = {
45
45
  /**
46
46
  * Properties to exclude using `.omit({ key: true })`.
47
47
  */
48
- keysToOmit?: Array<string>
48
+ keysToOmit?: Array<string> | null
49
49
  /**
50
50
  * Schema names that form circular dependency chains.
51
51
  * Properties referencing these emit lazy getters wrapping refs in `z.lazy(() => …)`.
@@ -61,6 +61,22 @@ export type PrinterZodMiniOptions = {
61
61
  * Factory options for the Zod Mini printer, defining input/output types and configuration.
62
62
  */
63
63
  export type PrinterZodMiniFactory = ast.PrinterFactoryOptions<'zod-mini', PrinterZodMiniOptions, string, string>
64
+
65
+ function strictOneOfMember(member: string, node: ast.SchemaNode): string {
66
+ if (node.type === 'object' && (node.additionalProperties === undefined || node.additionalProperties === false)) {
67
+ return member.replace(/^z\.object\(/, 'z.strictObject(')
68
+ }
69
+
70
+ return member
71
+ }
72
+
73
+ function getMemberConstraintMini(member: ast.SchemaNode): string | undefined {
74
+ if (member.primitive === 'string') return lengthChecksMini(ast.narrowSchema(member, 'string') ?? {}) || undefined
75
+ if (member.primitive === 'number' || member.primitive === 'integer')
76
+ return numberChecksMini(ast.narrowSchema(member, 'number') ?? ast.narrowSchema(member, 'integer') ?? {}) || undefined
77
+ if (member.primitive === 'array') return lengthChecksMini(ast.narrowSchema(member, 'array') ?? {}) || undefined
78
+ }
79
+
64
80
  /**
65
81
  * Zod v4 **Mini** printer built with `definePrinter`.
66
82
  *
@@ -144,8 +160,8 @@ export const printerZodMini = ast.definePrinter<PrinterZodMiniFactory>((options)
144
160
  },
145
161
 
146
162
  ref(node) {
147
- if (!node.name) return undefined
148
- const refName = node.ref ? (ast.extractRefName(node.ref) ?? node.name) : node.name
163
+ if (!node.name) return null
164
+ const refName = node.ref ? (extractRefName(node.ref) ?? node.name) : node.name
149
165
  const resolvedName = node.ref ? (this.options.resolver?.default(refName, 'function') ?? refName) : node.name
150
166
 
151
167
  if (node.ref && this.options.cyclicSchemas?.has(refName)) {
@@ -155,70 +171,73 @@ export const printerZodMini = ast.definePrinter<PrinterZodMiniFactory>((options)
155
171
  return resolvedName
156
172
  },
157
173
  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}`
174
+ const entries = node.properties.map((prop) => {
175
+ const { name: propName, schema } = prop
176
+
177
+ const meta = ast.syncSchemaRef(schema)
178
+
179
+ const isNullable = meta.nullable
180
+ const isOptional = schema.optional
181
+ const isNullish = schema.nullish
182
+
183
+ const hasSelfRef = this.options.cyclicSchemas != null && ast.containsCircularRef(schema, { circularSchemas: this.options.cyclicSchemas })
184
+ // Inside a getter the getter itself defers evaluation, so suppress
185
+ // z.lazy() wrapping on nested refs by temporarily clearing cyclicSchemas.
186
+ // Save before clearing: this.options === options (same reference via definePrinter),
187
+ // so reading options.cyclicSchemas after mutation would return undefined.
188
+ const savedCyclicSchemas = this.options.cyclicSchemas
189
+ if (hasSelfRef) this.options.cyclicSchemas = undefined
190
+ const baseOutput = this.transform(schema) ?? this.transform(ast.createSchema({ type: 'unknown' }))!
191
+ if (hasSelfRef) this.options.cyclicSchemas = savedCyclicSchemas
192
+
193
+ const wrappedOutput = this.options.wrapOutput ? this.options.wrapOutput({ output: baseOutput, schema }) || baseOutput : baseOutput
194
+
195
+ const value = applyMiniModifiers({
196
+ value: wrappedOutput,
197
+ nullable: isNullable,
198
+ optional: isOptional,
199
+ nullish: isNullish,
200
+ defaultValue: meta.default,
189
201
  })
190
- .join(',\n ')
191
202
 
192
- return `z.object({\n ${properties}\n })`
203
+ if (hasSelfRef) {
204
+ return `get ${objectKey(propName)}() { return ${value} }`
205
+ }
206
+ return `${objectKey(propName)}: ${value}`
207
+ })
208
+
209
+ return `z.object(${buildObject(entries)})`
193
210
  },
194
211
  array(node) {
195
212
  const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
196
213
  const inner = items.join(', ') || this.transform(ast.createSchema({ type: 'unknown' }))!
197
- let result = `z.array(${inner})${lengthChecksMini(node)}`
214
+ const base = `z.array(${inner})${lengthChecksMini(node)}`
198
215
 
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
216
+ return node.unique ? `${base}.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })` : base
204
217
  },
205
218
  tuple(node) {
206
219
  const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
207
220
 
208
- return `z.tuple([${items.join(', ')}])`
221
+ return `z.tuple(${buildList(items)})`
209
222
  },
210
223
  union(node) {
211
224
  const nodeMembers = node.members ?? []
212
- const members = nodeMembers.map((m) => this.transform(m)).filter(Boolean)
225
+ const members = nodeMembers
226
+ .map((memberNode) => {
227
+ const member = this.transform(memberNode)
228
+
229
+ return member && node.strategy === 'one' ? strictOneOfMember(member, memberNode) : member
230
+ })
231
+ .filter(Boolean)
213
232
  if (members.length === 0) return ''
214
233
  if (members.length === 1) return members[0]!
215
234
  if (node.discriminatorPropertyName && !nodeMembers.some((m) => m.type === 'intersection')) {
216
235
  // z.discriminatedUnion requires ZodObject members; intersections (ZodIntersection) are not
217
236
  // assignable to $ZodDiscriminant, so fall back to z.union when any member is an intersection.
218
- return `z.discriminatedUnion(${stringify(node.discriminatorPropertyName)}, [${members.join(', ')}])`
237
+ return `z.discriminatedUnion(${stringify(node.discriminatorPropertyName)}, ${buildList(members)})`
219
238
  }
220
239
 
221
- return `z.union([${members.join(', ')}])`
240
+ return `z.union(${buildList(members)})`
222
241
  },
223
242
  intersection(node) {
224
243
  const members = node.members ?? []
@@ -227,61 +246,37 @@ export const printerZodMini = ast.definePrinter<PrinterZodMiniFactory>((options)
227
246
  const [first, ...rest] = members
228
247
  if (!first) return ''
229
248
 
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
- }
249
+ const firstBase = this.transform(first)
250
+ if (!firstBase) return ''
259
251
 
260
- return base
252
+ return rest.reduce((acc, member) => {
253
+ const constraint = getMemberConstraintMini(member)
254
+ if (constraint) return acc + constraint
255
+ const transformed = this.transform(member)
256
+ return transformed ? `z.intersection(${acc}, ${transformed})` : acc
257
+ }, firstBase)
261
258
  },
262
259
  ...options.nodes,
263
260
  },
264
261
  print(node) {
265
262
  const { keysToOmit } = this.options
266
263
 
267
- let base = this.transform(node)
268
- if (!base) return null
264
+ const transformed = this.transform(node)
265
+ if (!transformed) return null
269
266
 
270
267
  const meta = ast.syncSchemaRef(node)
271
268
 
272
- if (keysToOmit?.length && meta.primitive === 'object' && !(meta.type === 'union' && meta.discriminatorPropertyName)) {
269
+ const base = (() => {
270
+ if (!keysToOmit?.length || meta.primitive !== 'object' || (meta.type === 'union' && meta.discriminatorPropertyName)) return transformed
273
271
  // Mirror printerTs `nonNullable: true`: when omitting keys, the resulting
274
272
  // schema is a new non-nullable object type — skip optional/nullable/nullish.
275
273
  // Discriminated unions (z.discriminatedUnion) do not support .omit(), so skip them.
276
274
 
277
275
  // 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
- }
276
+ const lazyMatch = transformed.match(/^z\.lazy\(\(\)\s*=>\s*(.+)\)$/)
277
+ if (lazyMatch) return `z.lazy(() => ${lazyMatch[1]}.omit({ ${keysToOmit.map((k: string) => `"${k}": true`).join(', ')} }))`
278
+ return `${transformed}.omit({ ${keysToOmit.map((k: string) => `"${k}": true`).join(', ')} })`
279
+ })()
285
280
 
286
281
  return applyMiniModifiers({
287
282
  value: base,