@kubb/plugin-zod 5.0.0-beta.56 → 5.0.0-beta.64
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.
- package/dist/index.cjs +58 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +59 -54
- package/dist/index.js.map +1 -1
- package/package.json +7 -8
- package/src/components/Operations.tsx +0 -78
- package/src/components/Zod.tsx +0 -42
- package/src/constants.ts +0 -5
- package/src/generators/zodGenerator.tsx +0 -367
- package/src/index.ts +0 -11
- package/src/plugin.ts +0 -97
- package/src/printers/printerZod.ts +0 -360
- package/src/printers/printerZodMini.ts +0 -290
- package/src/resolvers/resolverZod.ts +0 -71
- package/src/types.ts +0 -219
- package/src/utils.ts +0 -299
|
@@ -1,360 +0,0 @@
|
|
|
1
|
-
import { buildList, buildObject, extractRefName, objectKey, stringify } from '@kubb/ast/utils'
|
|
2
|
-
|
|
3
|
-
import { ast } from '@kubb/core'
|
|
4
|
-
import type { PluginZod, ResolverZod } from '../types.ts'
|
|
5
|
-
import { applyModifiers, containsCodec, formatLiteral, getCodec, 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> | null
|
|
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
|
-
* 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'
|
|
71
|
-
/**
|
|
72
|
-
* Custom handler map for node type overrides.
|
|
73
|
-
*/
|
|
74
|
-
nodes?: PrinterZodNodes
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Factory options for the Zod printer, defining input/output types and configuration.
|
|
79
|
-
*/
|
|
80
|
-
export type PrinterZodFactory = ast.PrinterFactoryOptions<'zod', PrinterZodOptions, string, string>
|
|
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
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Zod v4 printer built with `definePrinter`.
|
|
111
|
-
*
|
|
112
|
-
* Converts a `SchemaNode` AST into a Zod v4 code string using the chainable API
|
|
113
|
-
* (`.optional()`, `.nullable()`, `.omit()`, etc.). For improved tree-shaking, see {@link printerZodMini}.
|
|
114
|
-
*
|
|
115
|
-
* @example Chainable API
|
|
116
|
-
* ```ts
|
|
117
|
-
* const printer = printerZod({ coercion: false })
|
|
118
|
-
* const code = printer.print(stringNode) // "z.string()"
|
|
119
|
-
* ```
|
|
120
|
-
*/
|
|
121
|
-
export const printerZod = ast.definePrinter<PrinterZodFactory>((options) => {
|
|
122
|
-
return {
|
|
123
|
-
name: 'zod',
|
|
124
|
-
options,
|
|
125
|
-
nodes: {
|
|
126
|
-
any: () => 'z.any()',
|
|
127
|
-
unknown: () => 'z.unknown()',
|
|
128
|
-
void: () => 'z.void()',
|
|
129
|
-
never: () => 'z.never()',
|
|
130
|
-
boolean: () => 'z.boolean()',
|
|
131
|
-
null: () => 'z.null()',
|
|
132
|
-
string(node) {
|
|
133
|
-
const base = shouldCoerce(this.options.coercion, 'strings') ? 'z.coerce.string()' : 'z.string()'
|
|
134
|
-
|
|
135
|
-
return `${base}${lengthConstraints(node)}`
|
|
136
|
-
},
|
|
137
|
-
number(node) {
|
|
138
|
-
const base = shouldCoerce(this.options.coercion, 'numbers') ? 'z.coerce.number()' : 'z.number()'
|
|
139
|
-
|
|
140
|
-
return `${base}${numberConstraints(node)}`
|
|
141
|
-
},
|
|
142
|
-
integer(node) {
|
|
143
|
-
const base = shouldCoerce(this.options.coercion, 'numbers') ? 'z.coerce.number().int()' : 'z.int()'
|
|
144
|
-
|
|
145
|
-
return `${base}${numberConstraints(node)}`
|
|
146
|
-
},
|
|
147
|
-
bigint() {
|
|
148
|
-
return shouldCoerce(this.options.coercion, 'numbers') ? 'z.coerce.bigint()' : 'z.bigint()'
|
|
149
|
-
},
|
|
150
|
-
date(node) {
|
|
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)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return 'z.iso.date()'
|
|
158
|
-
},
|
|
159
|
-
datetime(node) {
|
|
160
|
-
const offset = node.offset || this.options.dateType === 'stringOffset'
|
|
161
|
-
const local = node.local || this.options.dateType === 'stringLocal'
|
|
162
|
-
|
|
163
|
-
if (offset) return 'z.iso.datetime({ offset: true })'
|
|
164
|
-
if (local) return 'z.iso.datetime({ local: true })'
|
|
165
|
-
|
|
166
|
-
return 'z.iso.datetime()'
|
|
167
|
-
},
|
|
168
|
-
time(node) {
|
|
169
|
-
if (node.representation === 'string') {
|
|
170
|
-
return 'z.iso.time()'
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return shouldCoerce(this.options.coercion, 'dates') ? 'z.coerce.date()' : 'z.date()'
|
|
174
|
-
},
|
|
175
|
-
uuid(node) {
|
|
176
|
-
const base = this.options.guidType === 'guid' ? 'z.guid()' : 'z.uuid()'
|
|
177
|
-
|
|
178
|
-
return `${base}${lengthConstraints(node)}`
|
|
179
|
-
},
|
|
180
|
-
email(node) {
|
|
181
|
-
return `z.email()${lengthConstraints(node)}`
|
|
182
|
-
},
|
|
183
|
-
url(node) {
|
|
184
|
-
return `z.url()${lengthConstraints(node)}`
|
|
185
|
-
},
|
|
186
|
-
ipv4: () => 'z.ipv4()',
|
|
187
|
-
ipv6: () => 'z.ipv6()',
|
|
188
|
-
blob: () => 'z.instanceof(File)',
|
|
189
|
-
enum(node) {
|
|
190
|
-
const values = node.namedEnumValues?.map((v) => v.value) ?? node.enumValues ?? []
|
|
191
|
-
const nonNullValues = values.filter((v): v is string | number | boolean => v !== null)
|
|
192
|
-
|
|
193
|
-
// asConst-style enum: use z.union([z.literal(…), …])
|
|
194
|
-
if (node.namedEnumValues?.length) {
|
|
195
|
-
const literals = nonNullValues.map((v) => `z.literal(${formatLiteral(v)})`)
|
|
196
|
-
|
|
197
|
-
if (literals.length === 1) return literals[0]!
|
|
198
|
-
return `z.union([${literals.join(', ')}])`
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Regular enum: use z.enum([…])
|
|
202
|
-
return `z.enum([${nonNullValues.map(formatLiteral).join(', ')}])`
|
|
203
|
-
},
|
|
204
|
-
ref(node) {
|
|
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
|
|
216
|
-
|
|
217
|
-
if (node.ref && this.options.cyclicSchemas?.has(refName)) {
|
|
218
|
-
return `z.lazy(() => ${resolvedName})`
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return resolvedName
|
|
222
|
-
},
|
|
223
|
-
object(node) {
|
|
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,
|
|
257
|
-
})
|
|
258
|
-
|
|
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)})`
|
|
266
|
-
|
|
267
|
-
// Handle additionalProperties as .catchall() or .strict()
|
|
268
|
-
const result = (() => {
|
|
269
|
-
if (node.additionalProperties && node.additionalProperties !== true) {
|
|
270
|
-
const catchallType = this.transform(node.additionalProperties)
|
|
271
|
-
return catchallType ? `${objectBase}.catchall(${catchallType})` : objectBase
|
|
272
|
-
}
|
|
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
|
-
})()
|
|
277
|
-
|
|
278
|
-
return result
|
|
279
|
-
},
|
|
280
|
-
array(node) {
|
|
281
|
-
const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
|
|
282
|
-
const inner = items.join(', ') || this.transform(ast.createSchema({ type: 'unknown' }))!
|
|
283
|
-
const base = `z.array(${inner})${lengthConstraints(node)}`
|
|
284
|
-
|
|
285
|
-
return node.unique ? `${base}.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })` : base
|
|
286
|
-
},
|
|
287
|
-
tuple(node) {
|
|
288
|
-
const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
|
|
289
|
-
|
|
290
|
-
return `z.tuple(${buildList(items)})`
|
|
291
|
-
},
|
|
292
|
-
union(node) {
|
|
293
|
-
const nodeMembers = node.members ?? []
|
|
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)
|
|
301
|
-
if (members.length === 0) return ''
|
|
302
|
-
if (members.length === 1) return members[0]!
|
|
303
|
-
if (node.discriminatorPropertyName && !nodeMembers.some((m) => m.type === 'intersection')) {
|
|
304
|
-
// z.discriminatedUnion requires ZodObject members; intersections (ZodIntersection) are not
|
|
305
|
-
// assignable to $ZodDiscriminant, so fall back to z.union when any member is an intersection.
|
|
306
|
-
return `z.discriminatedUnion(${stringify(node.discriminatorPropertyName)}, ${buildList(members)})`
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
return `z.union(${buildList(members)})`
|
|
310
|
-
},
|
|
311
|
-
intersection(node) {
|
|
312
|
-
const members = node.members ?? []
|
|
313
|
-
if (members.length === 0) return ''
|
|
314
|
-
|
|
315
|
-
const [first, ...rest] = members
|
|
316
|
-
if (!first) return ''
|
|
317
|
-
|
|
318
|
-
const firstBase = this.transform(first)
|
|
319
|
-
if (!firstBase) return ''
|
|
320
|
-
|
|
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)
|
|
327
|
-
},
|
|
328
|
-
...options.nodes,
|
|
329
|
-
},
|
|
330
|
-
print(node) {
|
|
331
|
-
const { keysToOmit } = this.options
|
|
332
|
-
|
|
333
|
-
const transformed = this.transform(node)
|
|
334
|
-
if (!transformed) return null
|
|
335
|
-
|
|
336
|
-
const meta = ast.syncSchemaRef(node)
|
|
337
|
-
|
|
338
|
-
const base = (() => {
|
|
339
|
-
if (!keysToOmit?.length || meta.primitive !== 'object' || (meta.type === 'union' && meta.discriminatorPropertyName)) return transformed
|
|
340
|
-
// Mirror printerTs `nonNullable: true`: when omitting keys, the resulting
|
|
341
|
-
// schema is a new non-nullable object type — skip optional/nullable/nullish.
|
|
342
|
-
// Discriminated unions (z.discriminatedUnion) do not support .omit(), so skip them.
|
|
343
|
-
|
|
344
|
-
// If this is a lazy reference, apply omit inside the lazy function
|
|
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
|
-
})()
|
|
349
|
-
|
|
350
|
-
return applyModifiers({
|
|
351
|
-
value: base,
|
|
352
|
-
nullable: meta.nullable,
|
|
353
|
-
optional: meta.optional,
|
|
354
|
-
nullish: meta.nullish,
|
|
355
|
-
defaultValue: meta.default,
|
|
356
|
-
description: meta.description,
|
|
357
|
-
})
|
|
358
|
-
},
|
|
359
|
-
}
|
|
360
|
-
})
|
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
import { buildList, buildObject, extractRefName, objectKey, stringify } from '@kubb/ast/utils'
|
|
2
|
-
|
|
3
|
-
import { ast } from '@kubb/core'
|
|
4
|
-
import type { PluginZod, ResolverZod } from '../types.ts'
|
|
5
|
-
import { applyMiniModifiers, formatLiteral, lengthChecksMini, numberChecksMini } from '../utils.ts'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Partial map of node-type overrides for the Zod Mini printer.
|
|
9
|
-
*
|
|
10
|
-
* Each key is a `SchemaType` string (e.g. `'date'`, `'string'`). The function
|
|
11
|
-
* replaces the built-in handler for that node type. Use `this.transform` to
|
|
12
|
-
* recurse into nested schema nodes, and `this.options` to read printer options.
|
|
13
|
-
*
|
|
14
|
-
* @example Override the `date` handler
|
|
15
|
-
* ```ts
|
|
16
|
-
* pluginZod({
|
|
17
|
-
* mini: true,
|
|
18
|
-
* printer: {
|
|
19
|
-
* nodes: {
|
|
20
|
-
* date(node) {
|
|
21
|
-
* return 'z.iso.date()'
|
|
22
|
-
* },
|
|
23
|
-
* },
|
|
24
|
-
* },
|
|
25
|
-
* })
|
|
26
|
-
* ```
|
|
27
|
-
*/
|
|
28
|
-
export type PrinterZodMiniNodes = ast.PrinterPartial<string, PrinterZodMiniOptions>
|
|
29
|
-
|
|
30
|
-
export type PrinterZodMiniOptions = {
|
|
31
|
-
/**
|
|
32
|
-
* Use `z.guid()` or `z.uuid()` for UUID/GUID validation.
|
|
33
|
-
*
|
|
34
|
-
* @default 'uuid'
|
|
35
|
-
*/
|
|
36
|
-
guidType?: PluginZod['resolvedOptions']['guidType']
|
|
37
|
-
/**
|
|
38
|
-
* Hook to transform generated Zod schema before output.
|
|
39
|
-
*/
|
|
40
|
-
wrapOutput?: PluginZod['resolvedOptions']['wrapOutput']
|
|
41
|
-
/**
|
|
42
|
-
* Transforms raw schema names into valid JavaScript identifiers.
|
|
43
|
-
*/
|
|
44
|
-
resolver?: ResolverZod
|
|
45
|
-
/**
|
|
46
|
-
* Properties to exclude using `.omit({ key: true })`.
|
|
47
|
-
*/
|
|
48
|
-
keysToOmit?: Array<string> | null
|
|
49
|
-
/**
|
|
50
|
-
* Schema names that form circular dependency chains.
|
|
51
|
-
* Properties referencing these emit lazy getters wrapping refs in `z.lazy(() => …)`.
|
|
52
|
-
*/
|
|
53
|
-
cyclicSchemas?: ReadonlySet<string>
|
|
54
|
-
/**
|
|
55
|
-
* Custom handler map for node type overrides.
|
|
56
|
-
*/
|
|
57
|
-
nodes?: PrinterZodMiniNodes
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Factory options for the Zod Mini printer, defining input/output types and configuration.
|
|
62
|
-
*/
|
|
63
|
-
export type PrinterZodMiniFactory = ast.PrinterFactoryOptions<'zod-mini', PrinterZodMiniOptions, string, string>
|
|
64
|
-
|
|
65
|
-
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
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Zod v4 **Mini** printer built with `definePrinter`.
|
|
82
|
-
*
|
|
83
|
-
* Converts a `SchemaNode` AST into a Zod v4 code string using the functional API
|
|
84
|
-
* (`z.optional(z.string())`) for improved tree-shaking. See {@link printerZod} for the chainable API.
|
|
85
|
-
*
|
|
86
|
-
* @example Functional Mini API
|
|
87
|
-
* ```ts
|
|
88
|
-
* const printer = printerZodMini({})
|
|
89
|
-
* const code = printer.print(optionalStringNode) // "z.optional(z.string())"
|
|
90
|
-
* ```
|
|
91
|
-
*/
|
|
92
|
-
export const printerZodMini = ast.definePrinter<PrinterZodMiniFactory>((options) => {
|
|
93
|
-
return {
|
|
94
|
-
name: 'zod-mini',
|
|
95
|
-
options,
|
|
96
|
-
nodes: {
|
|
97
|
-
any: () => 'z.any()',
|
|
98
|
-
unknown: () => 'z.unknown()',
|
|
99
|
-
void: () => 'z.void()',
|
|
100
|
-
never: () => 'z.never()',
|
|
101
|
-
boolean: () => 'z.boolean()',
|
|
102
|
-
null: () => 'z.null()',
|
|
103
|
-
string(node) {
|
|
104
|
-
return `z.string()${lengthChecksMini(node)}`
|
|
105
|
-
},
|
|
106
|
-
number(node) {
|
|
107
|
-
return `z.number()${numberChecksMini(node)}`
|
|
108
|
-
},
|
|
109
|
-
integer(node) {
|
|
110
|
-
return `z.int()${numberChecksMini(node)}`
|
|
111
|
-
},
|
|
112
|
-
bigint(node) {
|
|
113
|
-
return `z.bigint()${numberChecksMini(node)}`
|
|
114
|
-
},
|
|
115
|
-
date(node) {
|
|
116
|
-
if (node.representation === 'string') {
|
|
117
|
-
return 'z.iso.date()'
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return 'z.date()'
|
|
121
|
-
},
|
|
122
|
-
datetime() {
|
|
123
|
-
// Mini mode: datetime validation via z.string() (z.iso.datetime not available in mini)
|
|
124
|
-
return 'z.string()'
|
|
125
|
-
},
|
|
126
|
-
time(node) {
|
|
127
|
-
if (node.representation === 'string') {
|
|
128
|
-
return 'z.iso.time()'
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return 'z.date()'
|
|
132
|
-
},
|
|
133
|
-
uuid(node) {
|
|
134
|
-
const base = this.options.guidType === 'guid' ? 'z.guid()' : 'z.uuid()'
|
|
135
|
-
|
|
136
|
-
return `${base}${lengthChecksMini(node)}`
|
|
137
|
-
},
|
|
138
|
-
email(node) {
|
|
139
|
-
return `z.email()${lengthChecksMini(node)}`
|
|
140
|
-
},
|
|
141
|
-
url(node) {
|
|
142
|
-
return `z.url()${lengthChecksMini(node)}`
|
|
143
|
-
},
|
|
144
|
-
ipv4: () => 'z.ipv4()',
|
|
145
|
-
ipv6: () => 'z.ipv6()',
|
|
146
|
-
blob: () => 'z.instanceof(File)',
|
|
147
|
-
enum(node) {
|
|
148
|
-
const values = node.namedEnumValues?.map((v) => v.value) ?? node.enumValues ?? []
|
|
149
|
-
const nonNullValues = values.filter((v): v is string | number | boolean => v !== null)
|
|
150
|
-
|
|
151
|
-
// asConst-style enum: use z.union([z.literal(…), …])
|
|
152
|
-
if (node.namedEnumValues?.length) {
|
|
153
|
-
const literals = nonNullValues.map((v) => `z.literal(${formatLiteral(v)})`)
|
|
154
|
-
if (literals.length === 1) return literals[0]!
|
|
155
|
-
return `z.union([${literals.join(', ')}])`
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Regular enum: use z.enum([…])
|
|
159
|
-
return `z.enum([${nonNullValues.map(formatLiteral).join(', ')}])`
|
|
160
|
-
},
|
|
161
|
-
|
|
162
|
-
ref(node) {
|
|
163
|
-
if (!node.name) return null
|
|
164
|
-
const refName = node.ref ? (extractRefName(node.ref) ?? node.name) : node.name
|
|
165
|
-
const resolvedName = node.ref ? (this.options.resolver?.default(refName, 'function') ?? refName) : node.name
|
|
166
|
-
|
|
167
|
-
if (node.ref && this.options.cyclicSchemas?.has(refName)) {
|
|
168
|
-
return `z.lazy(() => ${resolvedName})`
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return resolvedName
|
|
172
|
-
},
|
|
173
|
-
object(node) {
|
|
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,
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
if (hasSelfRef) {
|
|
204
|
-
return `get ${objectKey(propName)}() { return ${value} }`
|
|
205
|
-
}
|
|
206
|
-
return `${objectKey(propName)}: ${value}`
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
return `z.object(${buildObject(entries)})`
|
|
210
|
-
},
|
|
211
|
-
array(node) {
|
|
212
|
-
const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
|
|
213
|
-
const inner = items.join(', ') || this.transform(ast.createSchema({ type: 'unknown' }))!
|
|
214
|
-
const base = `z.array(${inner})${lengthChecksMini(node)}`
|
|
215
|
-
|
|
216
|
-
return node.unique ? `${base}.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })` : base
|
|
217
|
-
},
|
|
218
|
-
tuple(node) {
|
|
219
|
-
const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
|
|
220
|
-
|
|
221
|
-
return `z.tuple(${buildList(items)})`
|
|
222
|
-
},
|
|
223
|
-
union(node) {
|
|
224
|
-
const nodeMembers = node.members ?? []
|
|
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)
|
|
232
|
-
if (members.length === 0) return ''
|
|
233
|
-
if (members.length === 1) return members[0]!
|
|
234
|
-
if (node.discriminatorPropertyName && !nodeMembers.some((m) => m.type === 'intersection')) {
|
|
235
|
-
// z.discriminatedUnion requires ZodObject members; intersections (ZodIntersection) are not
|
|
236
|
-
// assignable to $ZodDiscriminant, so fall back to z.union when any member is an intersection.
|
|
237
|
-
return `z.discriminatedUnion(${stringify(node.discriminatorPropertyName)}, ${buildList(members)})`
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return `z.union(${buildList(members)})`
|
|
241
|
-
},
|
|
242
|
-
intersection(node) {
|
|
243
|
-
const members = node.members ?? []
|
|
244
|
-
if (members.length === 0) return ''
|
|
245
|
-
|
|
246
|
-
const [first, ...rest] = members
|
|
247
|
-
if (!first) return ''
|
|
248
|
-
|
|
249
|
-
const firstBase = this.transform(first)
|
|
250
|
-
if (!firstBase) return ''
|
|
251
|
-
|
|
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)
|
|
258
|
-
},
|
|
259
|
-
...options.nodes,
|
|
260
|
-
},
|
|
261
|
-
print(node) {
|
|
262
|
-
const { keysToOmit } = this.options
|
|
263
|
-
|
|
264
|
-
const transformed = this.transform(node)
|
|
265
|
-
if (!transformed) return null
|
|
266
|
-
|
|
267
|
-
const meta = ast.syncSchemaRef(node)
|
|
268
|
-
|
|
269
|
-
const base = (() => {
|
|
270
|
-
if (!keysToOmit?.length || meta.primitive !== 'object' || (meta.type === 'union' && meta.discriminatorPropertyName)) return transformed
|
|
271
|
-
// Mirror printerTs `nonNullable: true`: when omitting keys, the resulting
|
|
272
|
-
// schema is a new non-nullable object type — skip optional/nullable/nullish.
|
|
273
|
-
// Discriminated unions (z.discriminatedUnion) do not support .omit(), so skip them.
|
|
274
|
-
|
|
275
|
-
// If this is a lazy reference, apply omit inside the lazy function
|
|
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
|
-
})()
|
|
280
|
-
|
|
281
|
-
return applyMiniModifiers({
|
|
282
|
-
value: base,
|
|
283
|
-
nullable: meta.nullable,
|
|
284
|
-
optional: meta.optional,
|
|
285
|
-
nullish: meta.nullish,
|
|
286
|
-
defaultValue: meta.default,
|
|
287
|
-
})
|
|
288
|
-
},
|
|
289
|
-
}
|
|
290
|
-
})
|