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

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,18 +1,25 @@
1
1
  import type { Adapter } from '@kubb/core'
2
2
  import { ast, defineGenerator } from '@kubb/core'
3
3
  import type { AdapterOas } from '@kubb/adapter-oas'
4
- import { File, jsxRenderer } from '@kubb/renderer-jsx'
4
+ import { File, jsxRendererSync } from '@kubb/renderer-jsx'
5
5
  import { Operations } from '../components/Operations.tsx'
6
6
  import { Zod } from '../components/Zod.tsx'
7
7
  import { ZOD_NAMESPACE_IMPORTS } from '../constants.ts'
8
8
  import { printerZod } from '../printers/printerZod.ts'
9
9
  import { printerZodMini } from '../printers/printerZodMini.ts'
10
- import type { PluginZod } from '../types'
10
+ import type { PluginZod, ResolverZod } from '../types'
11
11
  import { buildSchemaNames } from '../utils.ts'
12
12
 
13
+ type ZodPrinterEntry = { printer: ReturnType<typeof printerZod>; coercion: unknown; guidType: unknown; dateType: unknown }
14
+ type ZodMiniPrinterEntry = { printer: ReturnType<typeof printerZodMini>; guidType: unknown }
15
+
16
+ // Per-build caches: keyed on resolver (unique per plugin instance per build, GC'd when released)
17
+ const zodPrinterCache = new WeakMap<ResolverZod, ZodPrinterEntry>()
18
+ const zodMiniPrinterCache = new WeakMap<ResolverZod, ZodMiniPrinterEntry>()
19
+
13
20
  export const zodGenerator = defineGenerator<PluginZod>({
14
21
  name: 'zod',
15
- renderer: jsxRenderer,
22
+ renderer: jsxRendererSync,
16
23
  schema(node, ctx) {
17
24
  const { adapter, config, resolver, root } = ctx
18
25
  const { output, coercion, guidType, mini, wrapOutput, inferred, importPath, group, printer } = ctx.options
@@ -37,19 +44,35 @@ export const zodGenerator = defineGenerator<PluginZod>({
37
44
 
38
45
  const inferTypeName = inferred ? resolver.resolveSchemaTypeName(node.name) : undefined
39
46
 
40
- const cyclicSchemas = adapter.inputNode ? ast.findCircularSchemas(adapter.inputNode.schemas) : undefined
47
+ const cyclicSchemas = new Set<string>(ctx.meta.circularNames)
41
48
 
42
- const schemaPrinter = mini
43
- ? printerZodMini({ guidType, wrapOutput, resolver, cyclicSchemas, nodes: printer?.nodes })
44
- : printerZod({ coercion, guidType, dateType, wrapOutput, resolver, cyclicSchemas, nodes: printer?.nodes })
49
+ const schemaPrinter = mini ? getCachedMiniPrinter() : getCachedStdPrinter()
50
+ function getCachedStdPrinter() {
51
+ const cached = zodPrinterCache.get(resolver)
52
+ if (cached && cached.coercion === coercion && cached.guidType === guidType && cached.dateType === dateType) {
53
+ return cached.printer
54
+ }
55
+ const p = printerZod({ coercion, guidType, dateType, wrapOutput, resolver, cyclicSchemas, nodes: printer?.nodes })
56
+ zodPrinterCache.set(resolver, { printer: p, coercion, guidType, dateType })
57
+ return p
58
+ }
59
+ function getCachedMiniPrinter() {
60
+ const cached = zodMiniPrinterCache.get(resolver)
61
+ if (cached && cached.guidType === guidType) {
62
+ return cached.printer
63
+ }
64
+ const p = printerZodMini({ guidType, wrapOutput, resolver, cyclicSchemas, nodes: printer?.nodes })
65
+ zodMiniPrinterCache.set(resolver, { printer: p, guidType })
66
+ return p
67
+ }
45
68
 
46
69
  return (
47
70
  <File
48
71
  baseName={meta.file.baseName}
49
72
  path={meta.file.path}
50
73
  meta={meta.file.meta}
51
- banner={resolver.resolveBanner(adapter.inputNode, { output, config })}
52
- footer={resolver.resolveFooter(adapter.inputNode, { output, config })}
74
+ banner={resolver.resolveBanner(ctx.meta, { output, config })}
75
+ footer={resolver.resolveFooter(ctx.meta, { output, config })}
53
76
  >
54
77
  <File.Import name={isZodImport ? 'z' : ['z']} path={importPath} isNameSpace={isZodImport} />
55
78
  {mode === 'split' && imports.map((imp) => <File.Import key={[node.name, imp.path].join('-')} root={meta.file.path} path={imp.path} name={imp.name} />)}
@@ -72,9 +95,9 @@ export const zodGenerator = defineGenerator<PluginZod>({
72
95
  file: resolver.resolveFile({ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path }, { root, output, group }),
73
96
  } as const
74
97
 
75
- const cyclicSchemas = adapter.inputNode ? ast.findCircularSchemas(adapter.inputNode.schemas) : undefined
98
+ const cyclicSchemas = new Set<string>(ctx.meta.circularNames)
76
99
 
77
- function renderSchemaEntry({ schema, name, keysToOmit }: { schema: ast.SchemaNode | null; name: string; keysToOmit?: Array<string> }) {
100
+ function renderSchemaEntry({ schema, name, keysToOmit }: { schema: ast.SchemaNode | null; name: string; keysToOmit?: Array<string> | null }) {
78
101
  if (!schema) return null
79
102
 
80
103
  const inferTypeName = inferred ? resolver.resolveTypeName(name) : undefined
@@ -84,9 +107,19 @@ export const zodGenerator = defineGenerator<PluginZod>({
84
107
  path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }).path,
85
108
  }))
86
109
 
110
+ const cachedStd = zodPrinterCache.get(resolver)
111
+ const cachedMini = zodMiniPrinterCache.get(resolver)
87
112
  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 })
113
+ ? keysToOmit?.length
114
+ ? printerZodMini({ guidType, wrapOutput, resolver, keysToOmit, cyclicSchemas, nodes: printer?.nodes })
115
+ : cachedMini?.guidType === guidType
116
+ ? cachedMini.printer
117
+ : printerZodMini({ guidType, wrapOutput, resolver, cyclicSchemas, nodes: printer?.nodes })
118
+ : keysToOmit?.length
119
+ ? printerZod({ coercion, guidType, dateType, wrapOutput, resolver, keysToOmit, cyclicSchemas, nodes: printer?.nodes })
120
+ : cachedStd?.coercion === coercion && cachedStd?.guidType === guidType && cachedStd?.dateType === dateType
121
+ ? cachedStd.printer
122
+ : printerZod({ coercion, guidType, dateType, wrapOutput, resolver, cyclicSchemas, nodes: printer?.nodes })
90
123
 
91
124
  return (
92
125
  <>
@@ -159,8 +192,8 @@ export const zodGenerator = defineGenerator<PluginZod>({
159
192
  baseName={meta.file.baseName}
160
193
  path={meta.file.path}
161
194
  meta={meta.file.meta}
162
- banner={resolver.resolveBanner(adapter.inputNode, { output, config })}
163
- footer={resolver.resolveFooter(adapter.inputNode, { output, config })}
195
+ banner={resolver.resolveBanner(ctx.meta, { output, config })}
196
+ footer={resolver.resolveFooter(ctx.meta, { output, config })}
164
197
  >
165
198
  <File.Import name={isZodImport ? 'z' : ['z']} path={importPath} isNameSpace={isZodImport} />
166
199
  {paramSchemas}
@@ -171,7 +204,7 @@ export const zodGenerator = defineGenerator<PluginZod>({
171
204
  )
172
205
  },
173
206
  operations(nodes, ctx) {
174
- const { adapter, config, resolver, root } = ctx
207
+ const { config, resolver, root } = ctx
175
208
  const { output, importPath, group, operations, paramsCasing } = ctx.options
176
209
 
177
210
  if (!operations) {
@@ -204,8 +237,8 @@ export const zodGenerator = defineGenerator<PluginZod>({
204
237
  baseName={meta.file.baseName}
205
238
  path={meta.file.path}
206
239
  meta={meta.file.meta}
207
- banner={resolver.resolveBanner(adapter.inputNode, { output, config })}
208
- footer={resolver.resolveFooter(adapter.inputNode, { output, config })}
240
+ banner={resolver.resolveBanner(ctx.meta, { output, config })}
241
+ footer={resolver.resolveFooter(ctx.meta, { output, config })}
209
242
  >
210
243
  <File.Import isTypeOnly name={isZodImport ? 'z' : ['z']} path={importPath} isNameSpace={isZodImport} />
211
244
  {imports}
@@ -53,7 +53,7 @@ export type PrinterZodOptions = {
53
53
  /**
54
54
  * Properties to exclude using `.omit({ key: true })`.
55
55
  */
56
- keysToOmit?: Array<string>
56
+ keysToOmit?: Array<string> | null
57
57
  /**
58
58
  * Schema names that form circular dependency chains.
59
59
  * Properties referencing these emit lazy getters wrapping refs in `z.lazy(() => …)`.
@@ -90,6 +90,13 @@ function strictOneOfMember(member: string, node: ast.SchemaNode): string {
90
90
  return member
91
91
  }
92
92
 
93
+ function getMemberConstraint(member: ast.SchemaNode): string | undefined {
94
+ if (member.primitive === 'string') return lengthConstraints(ast.narrowSchema(member, 'string') ?? {}) || undefined
95
+ if (member.primitive === 'number' || member.primitive === 'integer')
96
+ return numberConstraints(ast.narrowSchema(member, 'number') ?? ast.narrowSchema(member, 'integer') ?? {}) || undefined
97
+ if (member.primitive === 'array') return lengthConstraints(ast.narrowSchema(member, 'array') ?? {}) || undefined
98
+ }
99
+
93
100
  /**
94
101
  * Zod v4 printer built with `definePrinter`.
95
102
  *
@@ -184,7 +191,7 @@ export const printerZod = ast.definePrinter<PrinterZodFactory>((options) => {
184
191
  return `z.enum([${nonNullValues.map(formatLiteral).join(', ')}])`
185
192
  },
186
193
  ref(node) {
187
- if (!node.name) return undefined
194
+ if (!node.name) return null
188
195
  const refName = node.ref ? (ast.extractRefName(node.ref) ?? node.name) : node.name
189
196
  const resolvedName = node.ref ? (this.options.resolver?.default(refName, 'function') ?? refName) : node.name
190
197
 
@@ -208,19 +215,19 @@ export const printerZod = ast.definePrinter<PrinterZodFactory>((options) => {
208
215
  const hasSelfRef = this.options.cyclicSchemas != null && ast.containsCircularRef(schema, { circularSchemas: this.options.cyclicSchemas })
209
216
  // Inside a getter the getter itself defers evaluation, so suppress
210
217
  // z.lazy() wrapping on nested refs by temporarily clearing cyclicSchemas.
218
+ // Save before clearing: this.options === options (same reference via definePrinter),
219
+ // so reading options.cyclicSchemas after mutation would return undefined.
220
+ const savedCyclicSchemas = this.options.cyclicSchemas
211
221
  if (hasSelfRef) this.options.cyclicSchemas = undefined
212
222
  const baseOutput = this.transform(schema) ?? this.transform(ast.createSchema({ type: 'unknown' }))!
213
- if (hasSelfRef) this.options.cyclicSchemas = options.cyclicSchemas
223
+ if (hasSelfRef) this.options.cyclicSchemas = savedCyclicSchemas
214
224
 
215
225
  const wrappedOutput = this.options.wrapOutput ? this.options.wrapOutput({ output: baseOutput, schema }) || baseOutput : baseOutput
216
226
 
217
227
  // When a property schema is not a ref but the metadata is from a ref (e.g., discriminator
218
228
  // property override), skip applying the description from the ref target to avoid applying
219
229
  // metadata from a replaced schema.
220
- let descriptionToApply = meta.description
221
- if (schema.type !== 'ref' && meta.type === 'ref') {
222
- descriptionToApply = undefined
223
- }
230
+ const descriptionToApply = schema.type !== 'ref' && meta.type === 'ref' ? undefined : meta.description
224
231
 
225
232
  const value = applyModifiers({
226
233
  value: wrappedOutput,
@@ -238,32 +245,27 @@ export const printerZod = ast.definePrinter<PrinterZodFactory>((options) => {
238
245
  })
239
246
  .join(',\n ')
240
247
 
241
- let result = `z.object({\n ${properties}\n })`
248
+ const objectBase = `z.object({\n ${properties}\n })`
242
249
 
243
250
  // 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})`
251
+ const result = (() => {
252
+ if (node.additionalProperties && node.additionalProperties !== true) {
253
+ const catchallType = this.transform(node.additionalProperties)
254
+ return catchallType ? `${objectBase}.catchall(${catchallType})` : objectBase
248
255
  }
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
- }
256
+ if (node.additionalProperties === true) return `${objectBase}.catchall(${this.transform(ast.createSchema({ type: 'unknown' }))})`
257
+ if (node.additionalProperties === false) return `${objectBase}.strict()`
258
+ return objectBase
259
+ })()
254
260
 
255
261
  return result
256
262
  },
257
263
  array(node) {
258
264
  const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
259
265
  const inner = items.join(', ') || this.transform(ast.createSchema({ type: 'unknown' }))!
260
- let result = `z.array(${inner})${lengthConstraints(node)}`
266
+ const base = `z.array(${inner})${lengthConstraints(node)}`
261
267
 
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
268
+ return node.unique ? `${base}.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })` : base
267
269
  },
268
270
  tuple(node) {
269
271
  const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
@@ -296,61 +298,37 @@ export const printerZod = ast.definePrinter<PrinterZodFactory>((options) => {
296
298
  const [first, ...rest] = members
297
299
  if (!first) return ''
298
300
 
299
- let base = this.transform(first)
300
- if (!base) return ''
301
+ const firstBase = this.transform(first)
302
+ if (!firstBase) return ''
301
303
 
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
- }
304
+ return rest.reduce((acc, member) => {
305
+ const constraint = getMemberConstraint(member)
306
+ if (constraint) return acc + constraint
325
307
  const transformed = this.transform(member)
326
- if (transformed) base = `${base}.and(${transformed})`
327
- }
328
-
329
- return base
308
+ return transformed ? `${acc}.and(${transformed})` : acc
309
+ }, firstBase)
330
310
  },
331
311
  ...options.nodes,
332
312
  },
333
313
  print(node) {
334
314
  const { keysToOmit } = this.options
335
315
 
336
- let base = this.transform(node)
337
- if (!base) return null
316
+ const transformed = this.transform(node)
317
+ if (!transformed) return null
338
318
 
339
319
  const meta = ast.syncSchemaRef(node)
340
320
 
341
- if (keysToOmit?.length && meta.primitive === 'object' && !(meta.type === 'union' && meta.discriminatorPropertyName)) {
321
+ const base = (() => {
322
+ if (!keysToOmit?.length || meta.primitive !== 'object' || (meta.type === 'union' && meta.discriminatorPropertyName)) return transformed
342
323
  // Mirror printerTs `nonNullable: true`: when omitting keys, the resulting
343
324
  // schema is a new non-nullable object type — skip optional/nullable/nullish.
344
325
  // Discriminated unions (z.discriminatedUnion) do not support .omit(), so skip them.
345
326
 
346
327
  // 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
- }
328
+ const lazyMatch = transformed.match(/^z\.lazy\(\(\)\s*=>\s*(.+)\)$/)
329
+ if (lazyMatch) return `z.lazy(() => ${lazyMatch[1]}.omit({ ${keysToOmit.map((k: string) => `"${k}": true`).join(', ')} }))`
330
+ return `${transformed}.omit({ ${keysToOmit.map((k: string) => `"${k}": true`).join(', ')} })`
331
+ })()
354
332
 
355
333
  return applyModifiers({
356
334
  value: base,
@@ -45,7 +45,7 @@ export type PrinterZodMiniOptions = {
45
45
  /**
46
46
  * Properties to exclude using `.omit({ key: true })`.
47
47
  */
48
- keysToOmit?: Array<string>
48
+ keysToOmit?: Array<string> | null
49
49
  /**
50
50
  * Schema names that form circular dependency chains.
51
51
  * Properties referencing these emit lazy getters wrapping refs in `z.lazy(() => …)`.
@@ -70,6 +70,13 @@ function strictOneOfMember(member: string, node: ast.SchemaNode): string {
70
70
  return member
71
71
  }
72
72
 
73
+ function getMemberConstraintMini(member: ast.SchemaNode): string | undefined {
74
+ if (member.primitive === 'string') return lengthChecksMini(ast.narrowSchema(member, 'string') ?? {}) || undefined
75
+ if (member.primitive === 'number' || member.primitive === 'integer')
76
+ return numberChecksMini(ast.narrowSchema(member, 'number') ?? ast.narrowSchema(member, 'integer') ?? {}) || undefined
77
+ if (member.primitive === 'array') return lengthChecksMini(ast.narrowSchema(member, 'array') ?? {}) || undefined
78
+ }
79
+
73
80
  /**
74
81
  * Zod v4 **Mini** printer built with `definePrinter`.
75
82
  *
@@ -153,7 +160,7 @@ export const printerZodMini = ast.definePrinter<PrinterZodMiniFactory>((options)
153
160
  },
154
161
 
155
162
  ref(node) {
156
- if (!node.name) return undefined
163
+ if (!node.name) return null
157
164
  const refName = node.ref ? (ast.extractRefName(node.ref) ?? node.name) : node.name
158
165
  const resolvedName = node.ref ? (this.options.resolver?.default(refName, 'function') ?? refName) : node.name
159
166
 
@@ -177,9 +184,12 @@ export const printerZodMini = ast.definePrinter<PrinterZodMiniFactory>((options)
177
184
  const hasSelfRef = this.options.cyclicSchemas != null && ast.containsCircularRef(schema, { circularSchemas: this.options.cyclicSchemas })
178
185
  // Inside a getter the getter itself defers evaluation, so suppress
179
186
  // z.lazy() wrapping on nested refs by temporarily clearing cyclicSchemas.
187
+ // Save before clearing: this.options === options (same reference via definePrinter),
188
+ // so reading options.cyclicSchemas after mutation would return undefined.
189
+ const savedCyclicSchemas = this.options.cyclicSchemas
180
190
  if (hasSelfRef) this.options.cyclicSchemas = undefined
181
191
  const baseOutput = this.transform(schema) ?? this.transform(ast.createSchema({ type: 'unknown' }))!
182
- if (hasSelfRef) this.options.cyclicSchemas = options.cyclicSchemas
192
+ if (hasSelfRef) this.options.cyclicSchemas = savedCyclicSchemas
183
193
 
184
194
  const wrappedOutput = this.options.wrapOutput ? this.options.wrapOutput({ output: baseOutput, schema }) || baseOutput : baseOutput
185
195
 
@@ -203,13 +213,9 @@ export const printerZodMini = ast.definePrinter<PrinterZodMiniFactory>((options)
203
213
  array(node) {
204
214
  const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
205
215
  const inner = items.join(', ') || this.transform(ast.createSchema({ type: 'unknown' }))!
206
- let result = `z.array(${inner})${lengthChecksMini(node)}`
216
+ const base = `z.array(${inner})${lengthChecksMini(node)}`
207
217
 
208
- if (node.unique) {
209
- result += `.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })`
210
- }
211
-
212
- return result
218
+ return node.unique ? `${base}.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })` : base
213
219
  },
214
220
  tuple(node) {
215
221
  const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
@@ -242,61 +248,37 @@ export const printerZodMini = ast.definePrinter<PrinterZodMiniFactory>((options)
242
248
  const [first, ...rest] = members
243
249
  if (!first) return ''
244
250
 
245
- let base = this.transform(first)
246
- if (!base) return ''
251
+ const firstBase = this.transform(first)
252
+ if (!firstBase) return ''
247
253
 
248
- for (const member of rest) {
249
- if (member.primitive === 'string') {
250
- const s = ast.narrowSchema(member, 'string')
251
- const c = lengthChecksMini(s ?? {})
252
- if (c) {
253
- base += c
254
- continue
255
- }
256
- } else if (member.primitive === 'number' || member.primitive === 'integer') {
257
- const n = ast.narrowSchema(member, 'number') ?? ast.narrowSchema(member, 'integer')
258
- const c = numberChecksMini(n ?? {})
259
- if (c) {
260
- base += c
261
- continue
262
- }
263
- } else if (member.primitive === 'array') {
264
- const a = ast.narrowSchema(member, 'array')
265
- const c = lengthChecksMini(a ?? {})
266
- if (c) {
267
- base += c
268
- continue
269
- }
270
- }
254
+ return rest.reduce((acc, member) => {
255
+ const constraint = getMemberConstraintMini(member)
256
+ if (constraint) return acc + constraint
271
257
  const transformed = this.transform(member)
272
- if (transformed) base = `z.intersection(${base}, ${transformed})`
273
- }
274
-
275
- return base
258
+ return transformed ? `z.intersection(${acc}, ${transformed})` : acc
259
+ }, firstBase)
276
260
  },
277
261
  ...options.nodes,
278
262
  },
279
263
  print(node) {
280
264
  const { keysToOmit } = this.options
281
265
 
282
- let base = this.transform(node)
283
- if (!base) return null
266
+ const transformed = this.transform(node)
267
+ if (!transformed) return null
284
268
 
285
269
  const meta = ast.syncSchemaRef(node)
286
270
 
287
- if (keysToOmit?.length && meta.primitive === 'object' && !(meta.type === 'union' && meta.discriminatorPropertyName)) {
271
+ const base = (() => {
272
+ if (!keysToOmit?.length || meta.primitive !== 'object' || (meta.type === 'union' && meta.discriminatorPropertyName)) return transformed
288
273
  // Mirror printerTs `nonNullable: true`: when omitting keys, the resulting
289
274
  // schema is a new non-nullable object type — skip optional/nullable/nullish.
290
275
  // Discriminated unions (z.discriminatedUnion) do not support .omit(), so skip them.
291
276
 
292
277
  // If this is a lazy reference, apply omit inside the lazy function
293
- const lazyMatch = base.match(/^z\.lazy\(\(\)\s*=>\s*(.+)\)$/)
294
- if (lazyMatch) {
295
- base = `z.lazy(() => ${lazyMatch[1]}.omit({ ${keysToOmit.map((k: string) => `"${k}": true`).join(', ')} }))`
296
- } else {
297
- base = `${base}.omit({ ${keysToOmit.map((k: string) => `"${k}": true`).join(', ')} })`
298
- }
299
- }
278
+ const lazyMatch = transformed.match(/^z\.lazy\(\(\)\s*=>\s*(.+)\)$/)
279
+ if (lazyMatch) return `z.lazy(() => ${lazyMatch[1]}.omit({ ${keysToOmit.map((k: string) => `"${k}": true`).join(', ')} }))`
280
+ return `${transformed}.omit({ ${keysToOmit.map((k: string) => `"${k}": true`).join(', ')} })`
281
+ })()
300
282
 
301
283
  return applyMiniModifiers({
302
284
  value: base,
package/src/utils.ts CHANGED
@@ -158,21 +158,14 @@ export function lengthChecksMini({ min, max, pattern }: LengthConstraints): stri
158
158
  * to a schema value string using the chainable Zod v4 API.
159
159
  */
160
160
  export function applyModifiers({ value, nullable, optional, nullish, defaultValue, description }: ModifierOptions): string {
161
- let result = value
162
- if (nullish || (nullable && optional)) {
163
- result = `${result}.nullish()`
164
- } else if (optional) {
165
- result = `${result}.optional()`
166
- } else if (nullable) {
167
- result = `${result}.nullable()`
168
- }
169
- if (defaultValue !== undefined) {
170
- result = `${result}.default(${formatDefault(defaultValue)})`
171
- }
172
- if (description) {
173
- result = `${result}.describe(${stringify(description)})`
174
- }
175
- return result
161
+ const withModifier = (() => {
162
+ if (nullish || (nullable && optional)) return `${value}.nullish()`
163
+ if (optional) return `${value}.optional()`
164
+ if (nullable) return `${value}.nullable()`
165
+ return value
166
+ })()
167
+ const withDefault = defaultValue !== undefined ? `${withModifier}.default(${formatDefault(defaultValue)})` : withModifier
168
+ return description ? `${withDefault}.describe(${stringify(description)})` : withDefault
176
169
  }
177
170
 
178
171
  /**
@@ -180,21 +173,12 @@ export function applyModifiers({ value, nullable, optional, nullish, defaultValu
180
173
  * (`z.nullable()`, `z.optional()`, `z.nullish()`).
181
174
  */
182
175
  export function applyMiniModifiers({ value, nullable, optional, nullish, defaultValue }: Omit<ModifierOptions, 'description'>): string {
183
- let result = value
184
- if (nullish) {
185
- result = `z.nullish(${result})`
186
- } else {
187
- if (nullable) {
188
- result = `z.nullable(${result})`
189
- }
190
- if (optional) {
191
- result = `z.optional(${result})`
192
- }
193
- }
194
- if (defaultValue !== undefined) {
195
- result = `z._default(${result}, ${formatDefault(defaultValue)})`
196
- }
197
- return result
176
+ const withModifier = (() => {
177
+ if (nullish) return `z.nullish(${value})`
178
+ const withNullable = nullable ? `z.nullable(${value})` : value
179
+ return optional ? `z.optional(${withNullable})` : withNullable
180
+ })()
181
+ return defaultValue !== undefined ? `z._default(${withModifier}, ${formatDefault(defaultValue)})` : withModifier
198
182
  }
199
183
 
200
184
  type BuildGroupedParamsSchemaOptions = {