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

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 (46) hide show
  1. package/LICENSE +17 -10
  2. package/README.md +25 -7
  3. package/dist/index.cjs +1087 -105
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.ts +369 -4
  6. package/dist/index.js +1074 -105
  7. package/dist/index.js.map +1 -1
  8. package/extension.yaml +485 -0
  9. package/package.json +43 -73
  10. package/src/components/Operations.tsx +25 -18
  11. package/src/components/Zod.tsx +21 -121
  12. package/src/constants.ts +5 -0
  13. package/src/generators/zodGenerator.tsx +174 -160
  14. package/src/index.ts +11 -2
  15. package/src/plugin.ts +67 -156
  16. package/src/printers/printerZod.ts +365 -0
  17. package/src/printers/printerZodMini.ts +310 -0
  18. package/src/resolvers/resolverZod.ts +57 -0
  19. package/src/types.ts +130 -115
  20. package/src/utils.ts +222 -0
  21. package/dist/components-B7zUFnAm.cjs +0 -890
  22. package/dist/components-B7zUFnAm.cjs.map +0 -1
  23. package/dist/components-eECfXVou.js +0 -842
  24. package/dist/components-eECfXVou.js.map +0 -1
  25. package/dist/components.cjs +0 -4
  26. package/dist/components.d.ts +0 -56
  27. package/dist/components.js +0 -2
  28. package/dist/generators-BjPDdJUz.cjs +0 -301
  29. package/dist/generators-BjPDdJUz.cjs.map +0 -1
  30. package/dist/generators-lTWPS6oN.js +0 -290
  31. package/dist/generators-lTWPS6oN.js.map +0 -1
  32. package/dist/generators.cjs +0 -4
  33. package/dist/generators.d.ts +0 -508
  34. package/dist/generators.js +0 -2
  35. package/dist/templates/ToZod.source.cjs +0 -7
  36. package/dist/templates/ToZod.source.cjs.map +0 -1
  37. package/dist/templates/ToZod.source.d.ts +0 -7
  38. package/dist/templates/ToZod.source.js +0 -6
  39. package/dist/templates/ToZod.source.js.map +0 -1
  40. package/dist/types-CoCoOc2u.d.ts +0 -172
  41. package/src/components/index.ts +0 -2
  42. package/src/generators/index.ts +0 -2
  43. package/src/generators/operationsGenerator.tsx +0 -50
  44. package/src/parser.ts +0 -909
  45. package/src/templates/ToZod.source.ts +0 -4
  46. package/templates/ToZod.ts +0 -61
package/src/plugin.ts CHANGED
@@ -1,183 +1,94 @@
1
- import path from 'node:path'
2
- import { camelCase, pascalCase } from '@internals/utils'
3
- import { createPlugin, type Group, getBarrelFiles, getMode, satisfiesDependency } from '@kubb/core'
4
- import { OperationGenerator, pluginOasName, SchemaGenerator } from '@kubb/plugin-oas'
5
- import { pluginTsName } from '@kubb/plugin-ts'
6
- import { operationsGenerator } from './generators'
1
+ import { camelCase } from '@internals/utils'
2
+ import { definePlugin, type Group } from '@kubb/core'
7
3
  import { zodGenerator } from './generators/zodGenerator.tsx'
8
- import { source as toZodSource } from './templates/ToZod.source.ts'
4
+ import { resolverZod } from './resolvers/resolverZod.ts'
9
5
  import type { PluginZod } from './types.ts'
10
6
 
7
+ /**
8
+ * Canonical plugin name for `@kubb/plugin-zod`, used in driver lookups and warnings.
9
+ */
11
10
  export const pluginZodName = 'plugin-zod' satisfies PluginZod['name']
12
11
 
13
- export const pluginZod = createPlugin<PluginZod>((options) => {
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) => {
14
26
  const {
15
27
  output = { path: 'zod', barrelType: 'named' },
16
28
  group,
17
29
  exclude = [],
18
30
  include,
19
31
  override = [],
20
- transformers = {},
21
- dateType = 'string',
22
- unknownType = 'any',
23
- emptySchemaType = unknownType,
24
- integerType = 'number',
25
32
  typed = false,
26
- mapper = {},
27
33
  operations = false,
28
34
  mini = false,
29
- version = mini ? '4' : satisfiesDependency('zod', '>=4') ? '4' : '3',
30
35
  guidType = 'uuid',
31
- importPath = mini ? 'zod/mini' : version === '4' ? 'zod/v4' : 'zod',
36
+ importPath = mini ? 'zod/mini' : 'zod',
32
37
  coercion = false,
33
38
  inferred = false,
34
- generators = [zodGenerator, operations ? operationsGenerator : undefined].filter(Boolean),
35
39
  wrapOutput = undefined,
36
- contentType,
40
+ paramsCasing,
41
+ printer,
42
+ resolver: userResolver,
43
+ transformer: userTransformer,
44
+ generators: userGenerators = [],
37
45
  } = options
38
46
 
39
- // @deprecated Will be removed in v5 when collisionDetection defaults to true
40
- const usedEnumNames = {}
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
41
58
 
42
59
  return {
43
60
  name: pluginZodName,
44
- options: {
45
- output,
46
- transformers,
47
- include,
48
- exclude,
49
- override,
50
- typed,
51
- dateType,
52
- unknownType,
53
- emptySchemaType,
54
- integerType,
55
- mapper,
56
- importPath,
57
- coercion,
58
- operations,
59
- inferred,
60
- group,
61
- wrapOutput,
62
- version,
63
- guidType,
64
- mini,
65
- usedEnumNames,
66
- },
67
- pre: [pluginOasName, typed ? pluginTsName : undefined].filter(Boolean),
68
- resolvePath(baseName, pathMode, options) {
69
- const root = path.resolve(this.config.root, this.config.output.path)
70
- const mode = pathMode ?? getMode(path.resolve(root, output.path))
71
-
72
- if (mode === 'single') {
73
- /**
74
- * when output is a file then we will always append to the same file(output file), see fileManager.addOrAppend
75
- * Other plugins then need to call addOrAppend instead of just add from the fileManager class
76
- */
77
- return path.resolve(root, output.path)
78
- }
79
-
80
- if (group && (options?.group?.path || options?.group?.tag)) {
81
- const groupName: Group['name'] = group?.name
82
- ? group.name
83
- : (ctx) => {
84
- if (group?.type === 'path') {
85
- return `${ctx.group.split('/')[1]}`
86
- }
87
- return `${camelCase(ctx.group)}Controller`
88
- }
89
-
90
- return path.resolve(
91
- root,
92
- output.path,
93
- groupName({
94
- group: group.type === 'path' ? options.group.path! : options.group.tag!,
95
- }),
96
- baseName,
97
- )
98
- }
99
-
100
- return path.resolve(root, output.path, baseName)
101
- },
102
- resolveName(name, type) {
103
- let resolvedName = camelCase(name, {
104
- suffix: type ? 'schema' : undefined,
105
- isFile: type === 'file',
106
- })
107
-
108
- if (type === 'type') {
109
- resolvedName = pascalCase(resolvedName)
110
- }
111
-
112
- if (type) {
113
- return transformers?.name?.(resolvedName, type) || resolvedName
114
- }
115
-
116
- return resolvedName
117
- },
118
- async install() {
119
- const root = path.resolve(this.config.root, this.config.output.path)
120
- const mode = getMode(path.resolve(root, output.path))
121
- const oas = await this.getOas()
122
-
123
- if (this.plugin.options.typed && this.plugin.options.version === '3') {
124
- // pre add bundled
125
- await this.addFile({
126
- baseName: 'ToZod.ts',
127
- path: path.resolve(root, '.kubb/ToZod.ts'),
128
- sources: [
129
- {
130
- name: 'ToZod',
131
- value: toZodSource,
132
- },
133
- ],
134
- imports: [],
135
- exports: [],
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,
136
80
  })
137
- }
138
-
139
- const schemaGenerator = new SchemaGenerator(this.plugin.options, {
140
- fabric: this.fabric,
141
- oas,
142
- driver: this.driver,
143
- events: this.events,
144
- plugin: this.plugin,
145
- contentType,
146
- include: undefined,
147
- override,
148
- mode,
149
- output: output.path,
150
- })
151
-
152
- const schemaFiles = await schemaGenerator.build(...generators)
153
- await this.upsertFile(...schemaFiles)
154
-
155
- const operationGenerator = new OperationGenerator(this.plugin.options, {
156
- fabric: this.fabric,
157
- oas,
158
- driver: this.driver,
159
- events: this.events,
160
- plugin: this.plugin,
161
- contentType,
162
- exclude,
163
- include,
164
- override,
165
- mode,
166
- })
167
-
168
- const operationFiles = await operationGenerator.build(...generators)
169
- await this.upsertFile(...operationFiles)
170
-
171
- const barrelFiles = await getBarrelFiles(this.fabric.files, {
172
- type: output.barrelType ?? 'named',
173
- root,
174
- output,
175
- meta: {
176
- pluginName: this.plugin.name,
177
- },
178
- })
179
-
180
- await this.upsertFile(...barrelFiles)
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
+ },
181
90
  },
182
91
  }
183
92
  })
93
+
94
+ export default pluginZod
@@ -0,0 +1,365 @@
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
+ })