@kubb/plugin-zod 5.0.0-beta.15 → 5.0.0-beta.25
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 +130 -133
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +79 -34
- package/dist/index.js +130 -133
- package/dist/index.js.map +1 -1
- package/extension.yaml +646 -185
- package/package.json +4 -4
- package/src/components/Operations.tsx +4 -4
- package/src/components/Zod.tsx +1 -1
- package/src/generators/zodGenerator.tsx +33 -21
- package/src/plugin.ts +21 -8
- package/src/printers/printerZod.ts +36 -61
- package/src/printers/printerZodMini.ts +26 -47
- package/src/resolvers/resolverZod.ts +9 -4
- package/src/types.ts +42 -21
- package/src/utils.ts +20 -36
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubb/plugin-zod",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.25",
|
|
4
4
|
"description": "Generate Zod validation schemas from your OpenAPI specification for runtime data parsing and type safety. Pairs perfectly with @kubb/plugin-ts for end-to-end type coverage.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"code-generation",
|
|
@@ -48,15 +48,15 @@
|
|
|
48
48
|
"registry": "https://registry.npmjs.org/"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@kubb/core": "5.0.0-beta.
|
|
52
|
-
"@kubb/renderer-jsx": "5.0.0-beta.
|
|
51
|
+
"@kubb/core": "5.0.0-beta.25",
|
|
52
|
+
"@kubb/renderer-jsx": "5.0.0-beta.25",
|
|
53
53
|
"remeda": "^2.34.1"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@internals/utils": "0.0.0"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@kubb/renderer-jsx": "5.0.0-beta.
|
|
59
|
+
"@kubb/renderer-jsx": "5.0.0-beta.25"
|
|
60
60
|
},
|
|
61
61
|
"size-limit": [
|
|
62
62
|
{
|
|
@@ -4,11 +4,11 @@ import { Const, File, Type } from '@kubb/renderer-jsx'
|
|
|
4
4
|
import type { KubbReactNode } from '@kubb/renderer-jsx/types'
|
|
5
5
|
|
|
6
6
|
type SchemaNames = {
|
|
7
|
-
request: string |
|
|
7
|
+
request: string | null
|
|
8
8
|
parameters: {
|
|
9
|
-
path: string |
|
|
10
|
-
query: string |
|
|
11
|
-
header: string |
|
|
9
|
+
path: string | null
|
|
10
|
+
query: string | null
|
|
11
|
+
header: string | null
|
|
12
12
|
}
|
|
13
13
|
responses: { default?: string } & Record<number | string, string>
|
|
14
14
|
errors: Record<number | string, string>
|
package/src/components/Zod.tsx
CHANGED
|
@@ -13,7 +13,7 @@ type Props = {
|
|
|
13
13
|
* then merges in any user-supplied `printer.nodes` overrides.
|
|
14
14
|
*/
|
|
15
15
|
printer: ast.Printer<PrinterZodFactory> | ast.Printer<PrinterZodMiniFactory>
|
|
16
|
-
inferTypeName?: string
|
|
16
|
+
inferTypeName?: string | null
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export function Zod({ name, node, printer, inferTypeName }: Props): KubbReactNode {
|
|
@@ -17,11 +17,17 @@ type ZodMiniPrinterEntry = { printer: ReturnType<typeof printerZodMini>; guidTyp
|
|
|
17
17
|
const zodPrinterCache = new WeakMap<ResolverZod, ZodPrinterEntry>()
|
|
18
18
|
const zodMiniPrinterCache = new WeakMap<ResolverZod, ZodMiniPrinterEntry>()
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Built-in generator for `@kubb/plugin-zod`. Emits one Zod schema per
|
|
22
|
+
* schema in the spec plus per-operation request/response/parameter schemas.
|
|
23
|
+
* When `mini: true`, schemas use the Zod Mini functional API instead of
|
|
24
|
+
* chainable methods.
|
|
25
|
+
*/
|
|
20
26
|
export const zodGenerator = defineGenerator<PluginZod>({
|
|
21
27
|
name: 'zod',
|
|
22
28
|
renderer: jsxRendererSync,
|
|
23
29
|
schema(node, ctx) {
|
|
24
|
-
const { adapter, config, resolver, root
|
|
30
|
+
const { adapter, config, resolver, root } = ctx
|
|
25
31
|
const { output, coercion, guidType, mini, wrapOutput, inferred, importPath, group, printer } = ctx.options
|
|
26
32
|
const dateType = (adapter as Adapter<AdapterOas>).options.dateType
|
|
27
33
|
|
|
@@ -34,17 +40,17 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
34
40
|
|
|
35
41
|
const imports = adapter.getImports(node, (schemaName) => ({
|
|
36
42
|
name: resolver.resolveSchemaName(schemaName),
|
|
37
|
-
path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }).path,
|
|
43
|
+
path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group: group ?? undefined }).path,
|
|
38
44
|
}))
|
|
39
45
|
|
|
40
46
|
const meta = {
|
|
41
47
|
name: resolver.resolveSchemaName(node.name),
|
|
42
|
-
file: resolver.resolveFile({ name: node.name, extname: '.ts' }, { root, output, group }),
|
|
48
|
+
file: resolver.resolveFile({ name: node.name, extname: '.ts' }, { root, output, group: group ?? undefined }),
|
|
43
49
|
} as const
|
|
44
50
|
|
|
45
|
-
const inferTypeName = inferred ? resolver.resolveSchemaTypeName(node.name) :
|
|
51
|
+
const inferTypeName = inferred ? resolver.resolveSchemaTypeName(node.name) : null
|
|
46
52
|
|
|
47
|
-
const cyclicSchemas =
|
|
53
|
+
const cyclicSchemas = new Set<string>(ctx.meta.circularNames)
|
|
48
54
|
|
|
49
55
|
const schemaPrinter = mini ? getCachedMiniPrinter() : getCachedStdPrinter()
|
|
50
56
|
function getCachedStdPrinter() {
|
|
@@ -71,8 +77,8 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
71
77
|
baseName={meta.file.baseName}
|
|
72
78
|
path={meta.file.path}
|
|
73
79
|
meta={meta.file.meta}
|
|
74
|
-
banner={resolver.resolveBanner(
|
|
75
|
-
footer={resolver.resolveFooter(
|
|
80
|
+
banner={resolver.resolveBanner(ctx.meta, { output, config })}
|
|
81
|
+
footer={resolver.resolveFooter(ctx.meta, { output, config })}
|
|
76
82
|
>
|
|
77
83
|
<File.Import name={isZodImport ? 'z' : ['z']} path={importPath} isNameSpace={isZodImport} />
|
|
78
84
|
{mode === 'split' && imports.map((imp) => <File.Import key={[node.name, imp.path].join('-')} root={meta.file.path} path={imp.path} name={imp.name} />)}
|
|
@@ -82,7 +88,7 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
82
88
|
)
|
|
83
89
|
},
|
|
84
90
|
operation(node, ctx) {
|
|
85
|
-
const { adapter, config, resolver, root
|
|
91
|
+
const { adapter, config, resolver, root } = ctx
|
|
86
92
|
const { output, coercion, guidType, mini, wrapOutput, inferred, importPath, group, paramsCasing, printer } = ctx.options
|
|
87
93
|
const dateType = (adapter as Adapter<AdapterOas>).options.dateType
|
|
88
94
|
|
|
@@ -92,19 +98,22 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
92
98
|
const params = ast.caseParams(node.parameters, paramsCasing)
|
|
93
99
|
|
|
94
100
|
const meta = {
|
|
95
|
-
file: resolver.resolveFile(
|
|
101
|
+
file: resolver.resolveFile(
|
|
102
|
+
{ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
|
|
103
|
+
{ root, output, group: group ?? undefined },
|
|
104
|
+
),
|
|
96
105
|
} as const
|
|
97
106
|
|
|
98
|
-
const cyclicSchemas =
|
|
107
|
+
const cyclicSchemas = new Set<string>(ctx.meta.circularNames)
|
|
99
108
|
|
|
100
|
-
function renderSchemaEntry({ schema, name, keysToOmit }: { schema: ast.SchemaNode | null; name: string; keysToOmit?: Array<string> }) {
|
|
109
|
+
function renderSchemaEntry({ schema, name, keysToOmit }: { schema: ast.SchemaNode | null; name: string; keysToOmit?: Array<string> | null }) {
|
|
101
110
|
if (!schema) return null
|
|
102
111
|
|
|
103
|
-
const inferTypeName = inferred ? resolver.resolveTypeName(name) :
|
|
112
|
+
const inferTypeName = inferred ? resolver.resolveTypeName(name) : null
|
|
104
113
|
|
|
105
114
|
const imports = adapter.getImports(schema, (schemaName) => ({
|
|
106
115
|
name: resolver.resolveSchemaName(schemaName),
|
|
107
|
-
path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }).path,
|
|
116
|
+
path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group: group ?? undefined }).path,
|
|
108
117
|
}))
|
|
109
118
|
|
|
110
119
|
const cachedStd = zodPrinterCache.get(resolver)
|
|
@@ -192,8 +201,8 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
192
201
|
baseName={meta.file.baseName}
|
|
193
202
|
path={meta.file.path}
|
|
194
203
|
meta={meta.file.meta}
|
|
195
|
-
banner={resolver.resolveBanner(
|
|
196
|
-
footer={resolver.resolveFooter(
|
|
204
|
+
banner={resolver.resolveBanner(ctx.meta, { output, config })}
|
|
205
|
+
footer={resolver.resolveFooter(ctx.meta, { output, config })}
|
|
197
206
|
>
|
|
198
207
|
<File.Import name={isZodImport ? 'z' : ['z']} path={importPath} isNameSpace={isZodImport} />
|
|
199
208
|
{paramSchemas}
|
|
@@ -204,7 +213,7 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
204
213
|
)
|
|
205
214
|
},
|
|
206
215
|
operations(nodes, ctx) {
|
|
207
|
-
const { config, resolver, root
|
|
216
|
+
const { config, resolver, root } = ctx
|
|
208
217
|
const { output, importPath, group, operations, paramsCasing } = ctx.options
|
|
209
218
|
|
|
210
219
|
if (!operations) {
|
|
@@ -213,7 +222,7 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
213
222
|
const isZodImport = ZOD_NAMESPACE_IMPORTS.has(importPath as 'zod' | 'zod/mini')
|
|
214
223
|
|
|
215
224
|
const meta = {
|
|
216
|
-
file: resolver.resolveFile({ name: 'operations', extname: '.ts' }, { root, output, group }),
|
|
225
|
+
file: resolver.resolveFile({ name: 'operations', extname: '.ts' }, { root, output, group: group ?? undefined }),
|
|
217
226
|
} as const
|
|
218
227
|
|
|
219
228
|
const transformedOperations = nodes.map((node) => {
|
|
@@ -226,8 +235,11 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
226
235
|
})
|
|
227
236
|
|
|
228
237
|
const imports = transformedOperations.flatMap(({ node, data }) => {
|
|
229
|
-
const names = [data.request, ...Object.values(data.responses), ...Object.values(data.parameters)].filter(Boolean) as string
|
|
230
|
-
const opFile = resolver.resolveFile(
|
|
238
|
+
const names = [data.request, ...Object.values(data.responses), ...Object.values(data.parameters)].filter(Boolean) as Array<string>
|
|
239
|
+
const opFile = resolver.resolveFile(
|
|
240
|
+
{ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
|
|
241
|
+
{ root, output, group: group ?? undefined },
|
|
242
|
+
)
|
|
231
243
|
|
|
232
244
|
return names.map((name) => <File.Import key={[name, opFile.path].join('-')} name={[name]} root={meta.file.path} path={opFile.path} />)
|
|
233
245
|
})
|
|
@@ -237,8 +249,8 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
237
249
|
baseName={meta.file.baseName}
|
|
238
250
|
path={meta.file.path}
|
|
239
251
|
meta={meta.file.meta}
|
|
240
|
-
banner={resolver.resolveBanner(
|
|
241
|
-
footer={resolver.resolveFooter(
|
|
252
|
+
banner={resolver.resolveBanner(ctx.meta, { output, config })}
|
|
253
|
+
footer={resolver.resolveFooter(ctx.meta, { output, config })}
|
|
242
254
|
>
|
|
243
255
|
<File.Import isTypeOnly name={isZodImport ? 'z' : ['z']} path={importPath} isNameSpace={isZodImport} />
|
|
244
256
|
{imports}
|
package/src/plugin.ts
CHANGED
|
@@ -5,20 +5,33 @@ import { resolverZod } from './resolvers/resolverZod.ts'
|
|
|
5
5
|
import type { PluginZod } from './types.ts'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Canonical plugin name for `@kubb/plugin-zod
|
|
8
|
+
* Canonical plugin name for `@kubb/plugin-zod`. Used for driver lookups and
|
|
9
|
+
* cross-plugin dependency references.
|
|
9
10
|
*/
|
|
10
11
|
export const pluginZodName = 'plugin-zod' satisfies PluginZod['name']
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
|
-
* Generates Zod
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* Generates Zod v4 schemas from an OpenAPI spec. Use them to validate API
|
|
15
|
+
* responses at runtime, build form schemas, or feed back into router libraries
|
|
16
|
+
* that consume Zod (tRPC, Hono, Elysia). Pair with `@kubb/plugin-client` and
|
|
17
|
+
* set the client's `parser: 'zod'` to validate every response automatically.
|
|
16
18
|
*
|
|
17
|
-
* @example
|
|
19
|
+
* @example
|
|
18
20
|
* ```ts
|
|
19
|
-
* import
|
|
21
|
+
* import { defineConfig } from 'kubb'
|
|
22
|
+
* import { pluginTs } from '@kubb/plugin-ts'
|
|
23
|
+
* import { pluginZod } from '@kubb/plugin-zod'
|
|
24
|
+
*
|
|
20
25
|
* export default defineConfig({
|
|
21
|
-
*
|
|
26
|
+
* input: { path: './petStore.yaml' },
|
|
27
|
+
* output: { path: './src/gen' },
|
|
28
|
+
* plugins: [
|
|
29
|
+
* pluginTs(),
|
|
30
|
+
* pluginZod({
|
|
31
|
+
* output: { path: './zod' },
|
|
32
|
+
* typed: true,
|
|
33
|
+
* }),
|
|
34
|
+
* ],
|
|
22
35
|
* })
|
|
23
36
|
* ```
|
|
24
37
|
*/
|
|
@@ -54,7 +67,7 @@ export const pluginZod = definePlugin<PluginZod>((options) => {
|
|
|
54
67
|
return `${camelCase(ctx.group)}Controller`
|
|
55
68
|
},
|
|
56
69
|
} satisfies Group)
|
|
57
|
-
:
|
|
70
|
+
: null
|
|
58
71
|
|
|
59
72
|
return {
|
|
60
73
|
name: pluginZodName,
|
|
@@ -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
|
|
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
|
|
|
@@ -220,10 +227,7 @@ export const printerZod = ast.definePrinter<PrinterZodFactory>((options) => {
|
|
|
220
227
|
// When a property schema is not a ref but the metadata is from a ref (e.g., discriminator
|
|
221
228
|
// property override), skip applying the description from the ref target to avoid applying
|
|
222
229
|
// metadata from a replaced schema.
|
|
223
|
-
|
|
224
|
-
if (schema.type !== 'ref' && meta.type === 'ref') {
|
|
225
|
-
descriptionToApply = undefined
|
|
226
|
-
}
|
|
230
|
+
const descriptionToApply = schema.type !== 'ref' && meta.type === 'ref' ? undefined : meta.description
|
|
227
231
|
|
|
228
232
|
const value = applyModifiers({
|
|
229
233
|
value: wrappedOutput,
|
|
@@ -241,32 +245,27 @@ export const printerZod = ast.definePrinter<PrinterZodFactory>((options) => {
|
|
|
241
245
|
})
|
|
242
246
|
.join(',\n ')
|
|
243
247
|
|
|
244
|
-
|
|
248
|
+
const objectBase = `z.object({\n ${properties}\n })`
|
|
245
249
|
|
|
246
250
|
// Handle additionalProperties as .catchall() or .strict()
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
+
const result = (() => {
|
|
252
|
+
if (node.additionalProperties && node.additionalProperties !== true) {
|
|
253
|
+
const catchallType = this.transform(node.additionalProperties)
|
|
254
|
+
return catchallType ? `${objectBase}.catchall(${catchallType})` : objectBase
|
|
251
255
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
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
|
+
})()
|
|
257
260
|
|
|
258
261
|
return result
|
|
259
262
|
},
|
|
260
263
|
array(node) {
|
|
261
264
|
const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
|
|
262
265
|
const inner = items.join(', ') || this.transform(ast.createSchema({ type: 'unknown' }))!
|
|
263
|
-
|
|
266
|
+
const base = `z.array(${inner})${lengthConstraints(node)}`
|
|
264
267
|
|
|
265
|
-
|
|
266
|
-
result += `.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })`
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
return result
|
|
268
|
+
return node.unique ? `${base}.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })` : base
|
|
270
269
|
},
|
|
271
270
|
tuple(node) {
|
|
272
271
|
const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
|
|
@@ -299,61 +298,37 @@ export const printerZod = ast.definePrinter<PrinterZodFactory>((options) => {
|
|
|
299
298
|
const [first, ...rest] = members
|
|
300
299
|
if (!first) return ''
|
|
301
300
|
|
|
302
|
-
|
|
303
|
-
if (!
|
|
301
|
+
const firstBase = this.transform(first)
|
|
302
|
+
if (!firstBase) return ''
|
|
304
303
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const c = lengthConstraints(s ?? {})
|
|
309
|
-
if (c) {
|
|
310
|
-
base += c
|
|
311
|
-
continue
|
|
312
|
-
}
|
|
313
|
-
} else if (member.primitive === 'number' || member.primitive === 'integer') {
|
|
314
|
-
const n = ast.narrowSchema(member, 'number') ?? ast.narrowSchema(member, 'integer')
|
|
315
|
-
const c = numberConstraints(n ?? {})
|
|
316
|
-
if (c) {
|
|
317
|
-
base += c
|
|
318
|
-
continue
|
|
319
|
-
}
|
|
320
|
-
} else if (member.primitive === 'array') {
|
|
321
|
-
const a = ast.narrowSchema(member, 'array')
|
|
322
|
-
const c = lengthConstraints(a ?? {})
|
|
323
|
-
if (c) {
|
|
324
|
-
base += c
|
|
325
|
-
continue
|
|
326
|
-
}
|
|
327
|
-
}
|
|
304
|
+
return rest.reduce((acc, member) => {
|
|
305
|
+
const constraint = getMemberConstraint(member)
|
|
306
|
+
if (constraint) return acc + constraint
|
|
328
307
|
const transformed = this.transform(member)
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
return base
|
|
308
|
+
return transformed ? `${acc}.and(${transformed})` : acc
|
|
309
|
+
}, firstBase)
|
|
333
310
|
},
|
|
334
311
|
...options.nodes,
|
|
335
312
|
},
|
|
336
313
|
print(node) {
|
|
337
314
|
const { keysToOmit } = this.options
|
|
338
315
|
|
|
339
|
-
|
|
340
|
-
if (!
|
|
316
|
+
const transformed = this.transform(node)
|
|
317
|
+
if (!transformed) return null
|
|
341
318
|
|
|
342
319
|
const meta = ast.syncSchemaRef(node)
|
|
343
320
|
|
|
344
|
-
|
|
321
|
+
const base = (() => {
|
|
322
|
+
if (!keysToOmit?.length || meta.primitive !== 'object' || (meta.type === 'union' && meta.discriminatorPropertyName)) return transformed
|
|
345
323
|
// Mirror printerTs `nonNullable: true`: when omitting keys, the resulting
|
|
346
324
|
// schema is a new non-nullable object type — skip optional/nullable/nullish.
|
|
347
325
|
// Discriminated unions (z.discriminatedUnion) do not support .omit(), so skip them.
|
|
348
326
|
|
|
349
327
|
// If this is a lazy reference, apply omit inside the lazy function
|
|
350
|
-
const lazyMatch =
|
|
351
|
-
if (lazyMatch) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
base = `${base}.omit({ ${keysToOmit.map((k: string) => `"${k}": true`).join(', ')} })`
|
|
355
|
-
}
|
|
356
|
-
}
|
|
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
|
+
})()
|
|
357
332
|
|
|
358
333
|
return applyModifiers({
|
|
359
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
|
|
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
|
|
|
@@ -206,13 +213,9 @@ export const printerZodMini = ast.definePrinter<PrinterZodMiniFactory>((options)
|
|
|
206
213
|
array(node) {
|
|
207
214
|
const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
|
|
208
215
|
const inner = items.join(', ') || this.transform(ast.createSchema({ type: 'unknown' }))!
|
|
209
|
-
|
|
216
|
+
const base = `z.array(${inner})${lengthChecksMini(node)}`
|
|
210
217
|
|
|
211
|
-
|
|
212
|
-
result += `.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })`
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return result
|
|
218
|
+
return node.unique ? `${base}.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })` : base
|
|
216
219
|
},
|
|
217
220
|
tuple(node) {
|
|
218
221
|
const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
|
|
@@ -245,61 +248,37 @@ export const printerZodMini = ast.definePrinter<PrinterZodMiniFactory>((options)
|
|
|
245
248
|
const [first, ...rest] = members
|
|
246
249
|
if (!first) return ''
|
|
247
250
|
|
|
248
|
-
|
|
249
|
-
if (!
|
|
251
|
+
const firstBase = this.transform(first)
|
|
252
|
+
if (!firstBase) return ''
|
|
250
253
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const c = lengthChecksMini(s ?? {})
|
|
255
|
-
if (c) {
|
|
256
|
-
base += c
|
|
257
|
-
continue
|
|
258
|
-
}
|
|
259
|
-
} else if (member.primitive === 'number' || member.primitive === 'integer') {
|
|
260
|
-
const n = ast.narrowSchema(member, 'number') ?? ast.narrowSchema(member, 'integer')
|
|
261
|
-
const c = numberChecksMini(n ?? {})
|
|
262
|
-
if (c) {
|
|
263
|
-
base += c
|
|
264
|
-
continue
|
|
265
|
-
}
|
|
266
|
-
} else if (member.primitive === 'array') {
|
|
267
|
-
const a = ast.narrowSchema(member, 'array')
|
|
268
|
-
const c = lengthChecksMini(a ?? {})
|
|
269
|
-
if (c) {
|
|
270
|
-
base += c
|
|
271
|
-
continue
|
|
272
|
-
}
|
|
273
|
-
}
|
|
254
|
+
return rest.reduce((acc, member) => {
|
|
255
|
+
const constraint = getMemberConstraintMini(member)
|
|
256
|
+
if (constraint) return acc + constraint
|
|
274
257
|
const transformed = this.transform(member)
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
return base
|
|
258
|
+
return transformed ? `z.intersection(${acc}, ${transformed})` : acc
|
|
259
|
+
}, firstBase)
|
|
279
260
|
},
|
|
280
261
|
...options.nodes,
|
|
281
262
|
},
|
|
282
263
|
print(node) {
|
|
283
264
|
const { keysToOmit } = this.options
|
|
284
265
|
|
|
285
|
-
|
|
286
|
-
if (!
|
|
266
|
+
const transformed = this.transform(node)
|
|
267
|
+
if (!transformed) return null
|
|
287
268
|
|
|
288
269
|
const meta = ast.syncSchemaRef(node)
|
|
289
270
|
|
|
290
|
-
|
|
271
|
+
const base = (() => {
|
|
272
|
+
if (!keysToOmit?.length || meta.primitive !== 'object' || (meta.type === 'union' && meta.discriminatorPropertyName)) return transformed
|
|
291
273
|
// Mirror printerTs `nonNullable: true`: when omitting keys, the resulting
|
|
292
274
|
// schema is a new non-nullable object type — skip optional/nullable/nullish.
|
|
293
275
|
// Discriminated unions (z.discriminatedUnion) do not support .omit(), so skip them.
|
|
294
276
|
|
|
295
277
|
// If this is a lazy reference, apply omit inside the lazy function
|
|
296
|
-
const lazyMatch =
|
|
297
|
-
if (lazyMatch) {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
base = `${base}.omit({ ${keysToOmit.map((k: string) => `"${k}": true`).join(', ')} })`
|
|
301
|
-
}
|
|
302
|
-
}
|
|
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
|
+
})()
|
|
303
282
|
|
|
304
283
|
return applyMiniModifiers({
|
|
305
284
|
value: base,
|
|
@@ -3,12 +3,17 @@ import { defineResolver } from '@kubb/core'
|
|
|
3
3
|
import type { PluginZod } from '../types.ts'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Default resolver used by `@kubb/plugin-zod`. Decides the names and file
|
|
7
|
+
* paths for every generated Zod schema. Schemas use camelCase with a
|
|
8
|
+
* `Schema` suffix (`listPetsSchema`); their inferred types use PascalCase.
|
|
7
9
|
*
|
|
8
|
-
*
|
|
10
|
+
* @example Resolve schema and type names
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { resolverZod } from '@kubb/plugin-zod'
|
|
9
13
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
14
|
+
* resolverZod.default('list pets', 'function') // 'listPetsSchema'
|
|
15
|
+
* resolverZod.resolveSchemaTypeName('pet') // 'PetSchema'
|
|
16
|
+
* ```
|
|
12
17
|
*/
|
|
13
18
|
export const resolverZod = defineResolver<PluginZod>(() => {
|
|
14
19
|
return {
|