@kubb/plugin-zod 5.0.0-beta.3 → 5.0.0-beta.30
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/README.md +25 -5
- package/dist/index.cjs +390 -177
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +79 -34
- package/dist/index.js +386 -179
- package/dist/index.js.map +1 -1
- package/extension.yaml +966 -0
- package/package.json +9 -13
- package/src/components/Operations.tsx +4 -4
- package/src/components/Zod.tsx +1 -1
- package/src/generators/zodGenerator.tsx +77 -32
- package/src/plugin.ts +21 -8
- package/src/printers/printerZod.ts +67 -63
- package/src/printers/printerZodMini.ts +46 -49
- package/src/resolvers/resolverZod.ts +25 -19
- package/src/types.ts +42 -21
- package/src/utils.ts +20 -36
package/package.json
CHANGED
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubb/plugin-zod",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
4
|
-
"description": "Zod
|
|
3
|
+
"version": "5.0.0-beta.30",
|
|
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
|
-
"code-
|
|
6
|
+
"code-generation",
|
|
7
7
|
"codegen",
|
|
8
8
|
"kubb",
|
|
9
|
-
"oas",
|
|
10
9
|
"openapi",
|
|
11
|
-
"
|
|
10
|
+
"parse",
|
|
12
11
|
"runtime-validation",
|
|
13
12
|
"schema",
|
|
14
|
-
"schema-validation",
|
|
15
13
|
"swagger",
|
|
16
|
-
"type-safe",
|
|
17
|
-
"type-safety",
|
|
18
14
|
"typescript",
|
|
19
15
|
"validation",
|
|
20
16
|
"zod"
|
|
@@ -30,7 +26,7 @@
|
|
|
30
26
|
"src",
|
|
31
27
|
"templates",
|
|
32
28
|
"dist",
|
|
33
|
-
"
|
|
29
|
+
"extension.yaml",
|
|
34
30
|
"!/**/**.test.**",
|
|
35
31
|
"!/**/__tests__/**",
|
|
36
32
|
"!/**/__snapshots__/**"
|
|
@@ -52,15 +48,15 @@
|
|
|
52
48
|
"registry": "https://registry.npmjs.org/"
|
|
53
49
|
},
|
|
54
50
|
"dependencies": {
|
|
55
|
-
"@kubb/core": "5.0.0-beta.
|
|
56
|
-
"@kubb/renderer-jsx": "5.0.0-beta.
|
|
57
|
-
"remeda": "^2.34.
|
|
51
|
+
"@kubb/core": "5.0.0-beta.29",
|
|
52
|
+
"@kubb/renderer-jsx": "5.0.0-beta.29",
|
|
53
|
+
"remeda": "^2.34.1"
|
|
58
54
|
},
|
|
59
55
|
"devDependencies": {
|
|
60
56
|
"@internals/utils": "0.0.0"
|
|
61
57
|
},
|
|
62
58
|
"peerDependencies": {
|
|
63
|
-
"@kubb/renderer-jsx": "5.0.0-beta.
|
|
59
|
+
"@kubb/renderer-jsx": "5.0.0-beta.29"
|
|
64
60
|
},
|
|
65
61
|
"size-limit": [
|
|
66
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 {
|
|
@@ -1,18 +1,31 @@
|
|
|
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,
|
|
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
|
+
|
|
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
|
+
*/
|
|
13
26
|
export const zodGenerator = defineGenerator<PluginZod>({
|
|
14
27
|
name: 'zod',
|
|
15
|
-
renderer:
|
|
28
|
+
renderer: jsxRendererSync,
|
|
16
29
|
schema(node, ctx) {
|
|
17
30
|
const { adapter, config, resolver, root } = ctx
|
|
18
31
|
const { output, coercion, guidType, mini, wrapOutput, inferred, importPath, group, printer } = ctx.options
|
|
@@ -27,29 +40,45 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
27
40
|
|
|
28
41
|
const imports = adapter.getImports(node, (schemaName) => ({
|
|
29
42
|
name: resolver.resolveSchemaName(schemaName),
|
|
30
|
-
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,
|
|
31
44
|
}))
|
|
32
45
|
|
|
33
46
|
const meta = {
|
|
34
47
|
name: resolver.resolveSchemaName(node.name),
|
|
35
|
-
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 }),
|
|
36
49
|
} as const
|
|
37
50
|
|
|
38
|
-
const inferTypeName = inferred ? resolver.resolveSchemaTypeName(node.name) :
|
|
51
|
+
const inferTypeName = inferred ? resolver.resolveSchemaTypeName(node.name) : null
|
|
39
52
|
|
|
40
|
-
const cyclicSchemas =
|
|
53
|
+
const cyclicSchemas = new Set<string>(ctx.meta.circularNames)
|
|
41
54
|
|
|
42
|
-
const schemaPrinter = mini
|
|
43
|
-
|
|
44
|
-
|
|
55
|
+
const schemaPrinter = mini ? getCachedMiniPrinter() : getCachedStdPrinter()
|
|
56
|
+
function getCachedStdPrinter() {
|
|
57
|
+
const cached = zodPrinterCache.get(resolver)
|
|
58
|
+
if (cached && cached.coercion === coercion && cached.guidType === guidType && cached.dateType === dateType) {
|
|
59
|
+
return cached.printer
|
|
60
|
+
}
|
|
61
|
+
const p = printerZod({ coercion, guidType, dateType, wrapOutput, resolver, cyclicSchemas, nodes: printer?.nodes })
|
|
62
|
+
zodPrinterCache.set(resolver, { printer: p, coercion, guidType, dateType })
|
|
63
|
+
return p
|
|
64
|
+
}
|
|
65
|
+
function getCachedMiniPrinter() {
|
|
66
|
+
const cached = zodMiniPrinterCache.get(resolver)
|
|
67
|
+
if (cached && cached.guidType === guidType) {
|
|
68
|
+
return cached.printer
|
|
69
|
+
}
|
|
70
|
+
const p = printerZodMini({ guidType, wrapOutput, resolver, cyclicSchemas, nodes: printer?.nodes })
|
|
71
|
+
zodMiniPrinterCache.set(resolver, { printer: p, guidType })
|
|
72
|
+
return p
|
|
73
|
+
}
|
|
45
74
|
|
|
46
75
|
return (
|
|
47
76
|
<File
|
|
48
77
|
baseName={meta.file.baseName}
|
|
49
78
|
path={meta.file.path}
|
|
50
79
|
meta={meta.file.meta}
|
|
51
|
-
banner={resolver.resolveBanner(
|
|
52
|
-
footer={resolver.resolveFooter(
|
|
80
|
+
banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
|
|
81
|
+
footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
|
|
53
82
|
>
|
|
54
83
|
<File.Import name={isZodImport ? 'z' : ['z']} path={importPath} isNameSpace={isZodImport} />
|
|
55
84
|
{mode === 'split' && imports.map((imp) => <File.Import key={[node.name, imp.path].join('-')} root={meta.file.path} path={imp.path} name={imp.name} />)}
|
|
@@ -69,24 +98,37 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
69
98
|
const params = ast.caseParams(node.parameters, paramsCasing)
|
|
70
99
|
|
|
71
100
|
const meta = {
|
|
72
|
-
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
|
+
),
|
|
73
105
|
} as const
|
|
74
106
|
|
|
75
|
-
const cyclicSchemas =
|
|
107
|
+
const cyclicSchemas = new Set<string>(ctx.meta.circularNames)
|
|
76
108
|
|
|
77
|
-
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 }) {
|
|
78
110
|
if (!schema) return null
|
|
79
111
|
|
|
80
|
-
const inferTypeName = inferred ? resolver.resolveTypeName(name) :
|
|
112
|
+
const inferTypeName = inferred ? resolver.resolveTypeName(name) : null
|
|
81
113
|
|
|
82
114
|
const imports = adapter.getImports(schema, (schemaName) => ({
|
|
83
115
|
name: resolver.resolveSchemaName(schemaName),
|
|
84
|
-
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,
|
|
85
117
|
}))
|
|
86
118
|
|
|
119
|
+
const cachedStd = zodPrinterCache.get(resolver)
|
|
120
|
+
const cachedMini = zodMiniPrinterCache.get(resolver)
|
|
87
121
|
const schemaPrinter = mini
|
|
88
|
-
?
|
|
89
|
-
|
|
122
|
+
? keysToOmit?.length
|
|
123
|
+
? printerZodMini({ guidType, wrapOutput, resolver, keysToOmit, cyclicSchemas, nodes: printer?.nodes })
|
|
124
|
+
: cachedMini?.guidType === guidType
|
|
125
|
+
? cachedMini.printer
|
|
126
|
+
: printerZodMini({ guidType, wrapOutput, resolver, cyclicSchemas, nodes: printer?.nodes })
|
|
127
|
+
: keysToOmit?.length
|
|
128
|
+
? printerZod({ coercion, guidType, dateType, wrapOutput, resolver, keysToOmit, cyclicSchemas, nodes: printer?.nodes })
|
|
129
|
+
: cachedStd?.coercion === coercion && cachedStd?.guidType === guidType && cachedStd?.dateType === dateType
|
|
130
|
+
? cachedStd.printer
|
|
131
|
+
: printerZod({ coercion, guidType, dateType, wrapOutput, resolver, cyclicSchemas, nodes: printer?.nodes })
|
|
90
132
|
|
|
91
133
|
return (
|
|
92
134
|
<>
|
|
@@ -101,13 +143,13 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
101
143
|
|
|
102
144
|
const responseSchemas = node.responses.map((res) =>
|
|
103
145
|
renderSchemaEntry({
|
|
104
|
-
schema: res.schema,
|
|
146
|
+
schema: res.content?.[0]?.schema ?? null,
|
|
105
147
|
name: resolver.resolveResponseStatusName(node, res.statusCode),
|
|
106
|
-
keysToOmit: res.keysToOmit,
|
|
148
|
+
keysToOmit: res.content?.[0]?.keysToOmit,
|
|
107
149
|
}),
|
|
108
150
|
)
|
|
109
151
|
|
|
110
|
-
const responsesWithSchema = node.responses.filter((res) => res.schema)
|
|
152
|
+
const responsesWithSchema = node.responses.filter((res) => res.content?.[0]?.schema)
|
|
111
153
|
const responseUnionSchema =
|
|
112
154
|
responsesWithSchema.length > 0
|
|
113
155
|
? (() => {
|
|
@@ -118,9 +160,9 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
118
160
|
// the response union name, skip generation to avoid redeclaration errors.
|
|
119
161
|
const importedNames = new Set(
|
|
120
162
|
responsesWithSchema.flatMap((res) =>
|
|
121
|
-
res.schema
|
|
163
|
+
res.content?.[0]?.schema
|
|
122
164
|
? adapter
|
|
123
|
-
.getImports(res.schema, (schemaName) => ({
|
|
165
|
+
.getImports(res.content[0].schema, (schemaName) => ({
|
|
124
166
|
name: resolver.resolveSchemaName(schemaName),
|
|
125
167
|
path: '',
|
|
126
168
|
}))
|
|
@@ -159,8 +201,8 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
159
201
|
baseName={meta.file.baseName}
|
|
160
202
|
path={meta.file.path}
|
|
161
203
|
meta={meta.file.meta}
|
|
162
|
-
banner={resolver.resolveBanner(
|
|
163
|
-
footer={resolver.resolveFooter(
|
|
204
|
+
banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
|
|
205
|
+
footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
|
|
164
206
|
>
|
|
165
207
|
<File.Import name={isZodImport ? 'z' : ['z']} path={importPath} isNameSpace={isZodImport} />
|
|
166
208
|
{paramSchemas}
|
|
@@ -171,7 +213,7 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
171
213
|
)
|
|
172
214
|
},
|
|
173
215
|
operations(nodes, ctx) {
|
|
174
|
-
const {
|
|
216
|
+
const { config, resolver, root } = ctx
|
|
175
217
|
const { output, importPath, group, operations, paramsCasing } = ctx.options
|
|
176
218
|
|
|
177
219
|
if (!operations) {
|
|
@@ -180,7 +222,7 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
180
222
|
const isZodImport = ZOD_NAMESPACE_IMPORTS.has(importPath as 'zod' | 'zod/mini')
|
|
181
223
|
|
|
182
224
|
const meta = {
|
|
183
|
-
file: resolver.resolveFile({ name: 'operations', extname: '.ts' }, { root, output, group }),
|
|
225
|
+
file: resolver.resolveFile({ name: 'operations', extname: '.ts' }, { root, output, group: group ?? undefined }),
|
|
184
226
|
} as const
|
|
185
227
|
|
|
186
228
|
const transformedOperations = nodes.map((node) => {
|
|
@@ -193,8 +235,11 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
193
235
|
})
|
|
194
236
|
|
|
195
237
|
const imports = transformedOperations.flatMap(({ node, data }) => {
|
|
196
|
-
const names = [data.request, ...Object.values(data.responses), ...Object.values(data.parameters)].filter(Boolean) as string
|
|
197
|
-
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
|
+
)
|
|
198
243
|
|
|
199
244
|
return names.map((name) => <File.Import key={[name, opFile.path].join('-')} name={[name]} root={meta.file.path} path={opFile.path} />)
|
|
200
245
|
})
|
|
@@ -204,8 +249,8 @@ export const zodGenerator = defineGenerator<PluginZod>({
|
|
|
204
249
|
baseName={meta.file.baseName}
|
|
205
250
|
path={meta.file.path}
|
|
206
251
|
meta={meta.file.meta}
|
|
207
|
-
banner={resolver.resolveBanner(
|
|
208
|
-
footer={resolver.resolveFooter(
|
|
252
|
+
banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
|
|
253
|
+
footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
|
|
209
254
|
>
|
|
210
255
|
<File.Import isTypeOnly name={isZodImport ? 'z' : ['z']} path={importPath} isNameSpace={isZodImport} />
|
|
211
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(() => …)`.
|
|
@@ -70,6 +70,33 @@ export type PrinterZodOptions = {
|
|
|
70
70
|
*/
|
|
71
71
|
export type PrinterZodFactory = ast.PrinterFactoryOptions<'zod', PrinterZodOptions, string, string>
|
|
72
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
|
+
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
|
+
|
|
73
100
|
/**
|
|
74
101
|
* Zod v4 printer built with `definePrinter`.
|
|
75
102
|
*
|
|
@@ -164,7 +191,7 @@ export const printerZod = ast.definePrinter<PrinterZodFactory>((options) => {
|
|
|
164
191
|
return `z.enum([${nonNullValues.map(formatLiteral).join(', ')}])`
|
|
165
192
|
},
|
|
166
193
|
ref(node) {
|
|
167
|
-
if (!node.name) return
|
|
194
|
+
if (!node.name) return null
|
|
168
195
|
const refName = node.ref ? (ast.extractRefName(node.ref) ?? node.name) : node.name
|
|
169
196
|
const resolvedName = node.ref ? (this.options.resolver?.default(refName, 'function') ?? refName) : node.name
|
|
170
197
|
|
|
@@ -188,19 +215,19 @@ export const printerZod = ast.definePrinter<PrinterZodFactory>((options) => {
|
|
|
188
215
|
const hasSelfRef = this.options.cyclicSchemas != null && ast.containsCircularRef(schema, { circularSchemas: this.options.cyclicSchemas })
|
|
189
216
|
// Inside a getter the getter itself defers evaluation, so suppress
|
|
190
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
|
|
191
221
|
if (hasSelfRef) this.options.cyclicSchemas = undefined
|
|
192
222
|
const baseOutput = this.transform(schema) ?? this.transform(ast.createSchema({ type: 'unknown' }))!
|
|
193
|
-
if (hasSelfRef) this.options.cyclicSchemas =
|
|
223
|
+
if (hasSelfRef) this.options.cyclicSchemas = savedCyclicSchemas
|
|
194
224
|
|
|
195
225
|
const wrappedOutput = this.options.wrapOutput ? this.options.wrapOutput({ output: baseOutput, schema }) || baseOutput : baseOutput
|
|
196
226
|
|
|
197
227
|
// When a property schema is not a ref but the metadata is from a ref (e.g., discriminator
|
|
198
228
|
// property override), skip applying the description from the ref target to avoid applying
|
|
199
229
|
// metadata from a replaced schema.
|
|
200
|
-
|
|
201
|
-
if (schema.type !== 'ref' && meta.type === 'ref') {
|
|
202
|
-
descriptionToApply = undefined
|
|
203
|
-
}
|
|
230
|
+
const descriptionToApply = schema.type !== 'ref' && meta.type === 'ref' ? undefined : meta.description
|
|
204
231
|
|
|
205
232
|
const value = applyModifiers({
|
|
206
233
|
value: wrappedOutput,
|
|
@@ -218,32 +245,27 @@ export const printerZod = ast.definePrinter<PrinterZodFactory>((options) => {
|
|
|
218
245
|
})
|
|
219
246
|
.join(',\n ')
|
|
220
247
|
|
|
221
|
-
|
|
248
|
+
const objectBase = `z.object({\n ${properties}\n })`
|
|
222
249
|
|
|
223
250
|
// Handle additionalProperties as .catchall() or .strict()
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
251
|
+
const result = (() => {
|
|
252
|
+
if (node.additionalProperties && node.additionalProperties !== true) {
|
|
253
|
+
const catchallType = this.transform(node.additionalProperties)
|
|
254
|
+
return catchallType ? `${objectBase}.catchall(${catchallType})` : objectBase
|
|
228
255
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
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
|
+
})()
|
|
234
260
|
|
|
235
261
|
return result
|
|
236
262
|
},
|
|
237
263
|
array(node) {
|
|
238
264
|
const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
|
|
239
265
|
const inner = items.join(', ') || this.transform(ast.createSchema({ type: 'unknown' }))!
|
|
240
|
-
|
|
266
|
+
const base = `z.array(${inner})${lengthConstraints(node)}`
|
|
241
267
|
|
|
242
|
-
|
|
243
|
-
result += `.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })`
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return result
|
|
268
|
+
return node.unique ? `${base}.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })` : base
|
|
247
269
|
},
|
|
248
270
|
tuple(node) {
|
|
249
271
|
const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
|
|
@@ -252,7 +274,13 @@ export const printerZod = ast.definePrinter<PrinterZodFactory>((options) => {
|
|
|
252
274
|
},
|
|
253
275
|
union(node) {
|
|
254
276
|
const nodeMembers = node.members ?? []
|
|
255
|
-
const members = nodeMembers
|
|
277
|
+
const members = nodeMembers
|
|
278
|
+
.map((memberNode) => {
|
|
279
|
+
const member = this.transform(memberNode)
|
|
280
|
+
|
|
281
|
+
return member && node.strategy === 'one' ? strictOneOfMember(member, memberNode) : member
|
|
282
|
+
})
|
|
283
|
+
.filter(Boolean)
|
|
256
284
|
if (members.length === 0) return ''
|
|
257
285
|
if (members.length === 1) return members[0]!
|
|
258
286
|
if (node.discriminatorPropertyName && !nodeMembers.some((m) => m.type === 'intersection')) {
|
|
@@ -270,61 +298,37 @@ export const printerZod = ast.definePrinter<PrinterZodFactory>((options) => {
|
|
|
270
298
|
const [first, ...rest] = members
|
|
271
299
|
if (!first) return ''
|
|
272
300
|
|
|
273
|
-
|
|
274
|
-
if (!
|
|
301
|
+
const firstBase = this.transform(first)
|
|
302
|
+
if (!firstBase) return ''
|
|
275
303
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const c = lengthConstraints(s ?? {})
|
|
280
|
-
if (c) {
|
|
281
|
-
base += c
|
|
282
|
-
continue
|
|
283
|
-
}
|
|
284
|
-
} else if (member.primitive === 'number' || member.primitive === 'integer') {
|
|
285
|
-
const n = ast.narrowSchema(member, 'number') ?? ast.narrowSchema(member, 'integer')
|
|
286
|
-
const c = numberConstraints(n ?? {})
|
|
287
|
-
if (c) {
|
|
288
|
-
base += c
|
|
289
|
-
continue
|
|
290
|
-
}
|
|
291
|
-
} else if (member.primitive === 'array') {
|
|
292
|
-
const a = ast.narrowSchema(member, 'array')
|
|
293
|
-
const c = lengthConstraints(a ?? {})
|
|
294
|
-
if (c) {
|
|
295
|
-
base += c
|
|
296
|
-
continue
|
|
297
|
-
}
|
|
298
|
-
}
|
|
304
|
+
return rest.reduce((acc, member) => {
|
|
305
|
+
const constraint = getMemberConstraint(member)
|
|
306
|
+
if (constraint) return acc + constraint
|
|
299
307
|
const transformed = this.transform(member)
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
return base
|
|
308
|
+
return transformed ? `${acc}.and(${transformed})` : acc
|
|
309
|
+
}, firstBase)
|
|
304
310
|
},
|
|
305
311
|
...options.nodes,
|
|
306
312
|
},
|
|
307
313
|
print(node) {
|
|
308
314
|
const { keysToOmit } = this.options
|
|
309
315
|
|
|
310
|
-
|
|
311
|
-
if (!
|
|
316
|
+
const transformed = this.transform(node)
|
|
317
|
+
if (!transformed) return null
|
|
312
318
|
|
|
313
319
|
const meta = ast.syncSchemaRef(node)
|
|
314
320
|
|
|
315
|
-
|
|
321
|
+
const base = (() => {
|
|
322
|
+
if (!keysToOmit?.length || meta.primitive !== 'object' || (meta.type === 'union' && meta.discriminatorPropertyName)) return transformed
|
|
316
323
|
// Mirror printerTs `nonNullable: true`: when omitting keys, the resulting
|
|
317
324
|
// schema is a new non-nullable object type — skip optional/nullable/nullish.
|
|
318
325
|
// Discriminated unions (z.discriminatedUnion) do not support .omit(), so skip them.
|
|
319
326
|
|
|
320
327
|
// If this is a lazy reference, apply omit inside the lazy function
|
|
321
|
-
const lazyMatch =
|
|
322
|
-
if (lazyMatch) {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
base = `${base}.omit({ ${keysToOmit.map((k: string) => `"${k}": true`).join(', ')} })`
|
|
326
|
-
}
|
|
327
|
-
}
|
|
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
|
+
})()
|
|
328
332
|
|
|
329
333
|
return applyModifiers({
|
|
330
334
|
value: base,
|