@kubb/plugin-zod 5.0.0-alpha.9 → 5.0.0-beta.4
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/LICENSE +17 -10
- package/README.md +1 -3
- package/dist/index.cjs +1061 -105
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +369 -4
- package/dist/index.js +1053 -104
- package/dist/index.js.map +1 -1
- package/extension.yaml +502 -0
- package/package.json +44 -70
- package/src/components/Operations.tsx +25 -18
- package/src/components/Zod.tsx +21 -121
- package/src/constants.ts +5 -0
- package/src/generators/zodGenerator.tsx +174 -160
- package/src/index.ts +11 -2
- package/src/plugin.ts +67 -156
- package/src/printers/printerZod.ts +339 -0
- package/src/printers/printerZodMini.ts +295 -0
- package/src/resolvers/resolverZod.ts +57 -0
- package/src/types.ts +130 -115
- package/src/utils.ts +222 -0
- package/dist/components-B7zUFnAm.cjs +0 -890
- package/dist/components-B7zUFnAm.cjs.map +0 -1
- package/dist/components-eECfXVou.js +0 -842
- package/dist/components-eECfXVou.js.map +0 -1
- package/dist/components.cjs +0 -4
- package/dist/components.d.ts +0 -56
- package/dist/components.js +0 -2
- package/dist/generators-BjPDdJUz.cjs +0 -301
- package/dist/generators-BjPDdJUz.cjs.map +0 -1
- package/dist/generators-lTWPS6oN.js +0 -290
- package/dist/generators-lTWPS6oN.js.map +0 -1
- package/dist/generators.cjs +0 -4
- package/dist/generators.d.ts +0 -508
- package/dist/generators.js +0 -2
- package/dist/templates/ToZod.source.cjs +0 -7
- package/dist/templates/ToZod.source.cjs.map +0 -1
- package/dist/templates/ToZod.source.d.ts +0 -7
- package/dist/templates/ToZod.source.js +0 -6
- package/dist/templates/ToZod.source.js.map +0 -1
- package/dist/types-CoCoOc2u.d.ts +0 -172
- package/src/components/index.ts +0 -2
- package/src/generators/index.ts +0 -2
- package/src/generators/operationsGenerator.tsx +0 -50
- package/src/parser.ts +0 -909
- package/src/templates/ToZod.source.ts +0 -4
- package/templates/ToZod.ts +0 -61
package/src/plugin.ts
CHANGED
|
@@ -1,183 +1,94 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
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' :
|
|
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
|
-
|
|
40
|
+
paramsCasing,
|
|
41
|
+
printer,
|
|
42
|
+
resolver: userResolver,
|
|
43
|
+
transformer: userTransformer,
|
|
44
|
+
generators: userGenerators = [],
|
|
37
45
|
} = options
|
|
38
46
|
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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,339 @@
|
|
|
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
|
+
/**
|
|
74
|
+
* Zod v4 printer built with `definePrinter`.
|
|
75
|
+
*
|
|
76
|
+
* Converts a `SchemaNode` AST into a Zod v4 code string using the chainable API
|
|
77
|
+
* (`.optional()`, `.nullable()`, `.omit()`, etc.). For improved tree-shaking, see {@link printerZodMini}.
|
|
78
|
+
*
|
|
79
|
+
* @example Chainable API
|
|
80
|
+
* ```ts
|
|
81
|
+
* const printer = printerZod({ coercion: false })
|
|
82
|
+
* const code = printer.print(stringNode) // "z.string()"
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export const printerZod = ast.definePrinter<PrinterZodFactory>((options) => {
|
|
86
|
+
return {
|
|
87
|
+
name: 'zod',
|
|
88
|
+
options,
|
|
89
|
+
nodes: {
|
|
90
|
+
any: () => 'z.any()',
|
|
91
|
+
unknown: () => 'z.unknown()',
|
|
92
|
+
void: () => 'z.void()',
|
|
93
|
+
never: () => 'z.never()',
|
|
94
|
+
boolean: () => 'z.boolean()',
|
|
95
|
+
null: () => 'z.null()',
|
|
96
|
+
string(node) {
|
|
97
|
+
const base = shouldCoerce(this.options.coercion, 'strings') ? 'z.coerce.string()' : 'z.string()'
|
|
98
|
+
|
|
99
|
+
return `${base}${lengthConstraints(node)}`
|
|
100
|
+
},
|
|
101
|
+
number(node) {
|
|
102
|
+
const base = shouldCoerce(this.options.coercion, 'numbers') ? 'z.coerce.number()' : 'z.number()'
|
|
103
|
+
|
|
104
|
+
return `${base}${numberConstraints(node)}`
|
|
105
|
+
},
|
|
106
|
+
integer(node) {
|
|
107
|
+
const base = shouldCoerce(this.options.coercion, 'numbers') ? 'z.coerce.number().int()' : 'z.int()'
|
|
108
|
+
|
|
109
|
+
return `${base}${numberConstraints(node)}`
|
|
110
|
+
},
|
|
111
|
+
bigint() {
|
|
112
|
+
return shouldCoerce(this.options.coercion, 'numbers') ? 'z.coerce.bigint()' : 'z.bigint()'
|
|
113
|
+
},
|
|
114
|
+
date(node) {
|
|
115
|
+
if (node.representation === 'string') {
|
|
116
|
+
return 'z.iso.date()'
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return shouldCoerce(this.options.coercion, 'dates') ? 'z.coerce.date()' : 'z.date()'
|
|
120
|
+
},
|
|
121
|
+
datetime(node) {
|
|
122
|
+
const offset = node.offset || this.options.dateType === 'stringOffset'
|
|
123
|
+
const local = node.local || this.options.dateType === 'stringLocal'
|
|
124
|
+
|
|
125
|
+
if (offset) return 'z.iso.datetime({ offset: true })'
|
|
126
|
+
if (local) return 'z.iso.datetime({ local: true })'
|
|
127
|
+
|
|
128
|
+
return 'z.iso.datetime()'
|
|
129
|
+
},
|
|
130
|
+
time(node) {
|
|
131
|
+
if (node.representation === 'string') {
|
|
132
|
+
return 'z.iso.time()'
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return shouldCoerce(this.options.coercion, 'dates') ? 'z.coerce.date()' : 'z.date()'
|
|
136
|
+
},
|
|
137
|
+
uuid(node) {
|
|
138
|
+
const base = this.options.guidType === 'guid' ? 'z.guid()' : 'z.uuid()'
|
|
139
|
+
|
|
140
|
+
return `${base}${lengthConstraints(node)}`
|
|
141
|
+
},
|
|
142
|
+
email(node) {
|
|
143
|
+
return `z.email()${lengthConstraints(node)}`
|
|
144
|
+
},
|
|
145
|
+
url(node) {
|
|
146
|
+
return `z.url()${lengthConstraints(node)}`
|
|
147
|
+
},
|
|
148
|
+
ipv4: () => 'z.ipv4()',
|
|
149
|
+
ipv6: () => 'z.ipv6()',
|
|
150
|
+
blob: () => 'z.instanceof(File)',
|
|
151
|
+
enum(node) {
|
|
152
|
+
const values = node.namedEnumValues?.map((v) => v.value) ?? node.enumValues ?? []
|
|
153
|
+
const nonNullValues = values.filter((v): v is string | number | boolean => v !== null)
|
|
154
|
+
|
|
155
|
+
// asConst-style enum: use z.union([z.literal(…), …])
|
|
156
|
+
if (node.namedEnumValues?.length) {
|
|
157
|
+
const literals = nonNullValues.map((v) => `z.literal(${formatLiteral(v)})`)
|
|
158
|
+
|
|
159
|
+
if (literals.length === 1) return literals[0]!
|
|
160
|
+
return `z.union([${literals.join(', ')}])`
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Regular enum: use z.enum([…])
|
|
164
|
+
return `z.enum([${nonNullValues.map(formatLiteral).join(', ')}])`
|
|
165
|
+
},
|
|
166
|
+
ref(node) {
|
|
167
|
+
if (!node.name) return undefined
|
|
168
|
+
const refName = node.ref ? (ast.extractRefName(node.ref) ?? node.name) : node.name
|
|
169
|
+
const resolvedName = node.ref ? (this.options.resolver?.default(refName, 'function') ?? refName) : node.name
|
|
170
|
+
|
|
171
|
+
if (node.ref && this.options.cyclicSchemas?.has(refName)) {
|
|
172
|
+
return `z.lazy(() => ${resolvedName})`
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return resolvedName
|
|
176
|
+
},
|
|
177
|
+
object(node) {
|
|
178
|
+
const properties = node.properties
|
|
179
|
+
.map((prop) => {
|
|
180
|
+
const { name: propName, schema } = prop
|
|
181
|
+
|
|
182
|
+
const meta = ast.syncSchemaRef(schema)
|
|
183
|
+
|
|
184
|
+
const isNullable = meta.nullable
|
|
185
|
+
const isOptional = schema.optional
|
|
186
|
+
const isNullish = schema.nullish
|
|
187
|
+
|
|
188
|
+
const hasSelfRef = this.options.cyclicSchemas != null && ast.containsCircularRef(schema, { circularSchemas: this.options.cyclicSchemas })
|
|
189
|
+
// Inside a getter the getter itself defers evaluation, so suppress
|
|
190
|
+
// z.lazy() wrapping on nested refs by temporarily clearing cyclicSchemas.
|
|
191
|
+
if (hasSelfRef) this.options.cyclicSchemas = undefined
|
|
192
|
+
const baseOutput = this.transform(schema) ?? this.transform(ast.createSchema({ type: 'unknown' }))!
|
|
193
|
+
if (hasSelfRef) this.options.cyclicSchemas = options.cyclicSchemas
|
|
194
|
+
|
|
195
|
+
const wrappedOutput = this.options.wrapOutput ? this.options.wrapOutput({ output: baseOutput, schema }) || baseOutput : baseOutput
|
|
196
|
+
|
|
197
|
+
// When a property schema is not a ref but the metadata is from a ref (e.g., discriminator
|
|
198
|
+
// property override), skip applying the description from the ref target to avoid applying
|
|
199
|
+
// metadata from a replaced schema.
|
|
200
|
+
let descriptionToApply = meta.description
|
|
201
|
+
if (schema.type !== 'ref' && meta.type === 'ref') {
|
|
202
|
+
descriptionToApply = undefined
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const value = applyModifiers({
|
|
206
|
+
value: wrappedOutput,
|
|
207
|
+
nullable: isNullable,
|
|
208
|
+
optional: isOptional,
|
|
209
|
+
nullish: isNullish,
|
|
210
|
+
defaultValue: meta.default,
|
|
211
|
+
description: descriptionToApply,
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
if (hasSelfRef) {
|
|
215
|
+
return `get "${propName}"() { return ${value} }`
|
|
216
|
+
}
|
|
217
|
+
return `"${propName}": ${value}`
|
|
218
|
+
})
|
|
219
|
+
.join(',\n ')
|
|
220
|
+
|
|
221
|
+
let result = `z.object({\n ${properties}\n })`
|
|
222
|
+
|
|
223
|
+
// Handle additionalProperties as .catchall() or .strict()
|
|
224
|
+
if (node.additionalProperties && node.additionalProperties !== true) {
|
|
225
|
+
const catchallType = this.transform(node.additionalProperties)
|
|
226
|
+
if (catchallType) {
|
|
227
|
+
result += `.catchall(${catchallType})`
|
|
228
|
+
}
|
|
229
|
+
} else if (node.additionalProperties === true) {
|
|
230
|
+
result += `.catchall(${this.transform(ast.createSchema({ type: 'unknown' }))})`
|
|
231
|
+
} else if (node.additionalProperties === false) {
|
|
232
|
+
result += '.strict()'
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return result
|
|
236
|
+
},
|
|
237
|
+
array(node) {
|
|
238
|
+
const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
|
|
239
|
+
const inner = items.join(', ') || this.transform(ast.createSchema({ type: 'unknown' }))!
|
|
240
|
+
let result = `z.array(${inner})${lengthConstraints(node)}`
|
|
241
|
+
|
|
242
|
+
if (node.unique) {
|
|
243
|
+
result += `.refine(items => new Set(items).size === items.length, { message: "Array entries must be unique" })`
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return result
|
|
247
|
+
},
|
|
248
|
+
tuple(node) {
|
|
249
|
+
const items = (node.items ?? []).map((item) => this.transform(item)).filter(Boolean)
|
|
250
|
+
|
|
251
|
+
return `z.tuple([${items.join(', ')}])`
|
|
252
|
+
},
|
|
253
|
+
union(node) {
|
|
254
|
+
const nodeMembers = node.members ?? []
|
|
255
|
+
const members = nodeMembers.map((m) => this.transform(m)).filter(Boolean)
|
|
256
|
+
if (members.length === 0) return ''
|
|
257
|
+
if (members.length === 1) return members[0]!
|
|
258
|
+
if (node.discriminatorPropertyName && !nodeMembers.some((m) => m.type === 'intersection')) {
|
|
259
|
+
// z.discriminatedUnion requires ZodObject members; intersections (ZodIntersection) are not
|
|
260
|
+
// assignable to $ZodDiscriminant, so fall back to z.union when any member is an intersection.
|
|
261
|
+
return `z.discriminatedUnion(${stringify(node.discriminatorPropertyName)}, [${members.join(', ')}])`
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return `z.union([${members.join(', ')}])`
|
|
265
|
+
},
|
|
266
|
+
intersection(node) {
|
|
267
|
+
const members = node.members ?? []
|
|
268
|
+
if (members.length === 0) return ''
|
|
269
|
+
|
|
270
|
+
const [first, ...rest] = members
|
|
271
|
+
if (!first) return ''
|
|
272
|
+
|
|
273
|
+
let base = this.transform(first)
|
|
274
|
+
if (!base) return ''
|
|
275
|
+
|
|
276
|
+
for (const member of rest) {
|
|
277
|
+
if (member.primitive === 'string') {
|
|
278
|
+
const s = ast.narrowSchema(member, 'string')
|
|
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
|
+
}
|
|
299
|
+
const transformed = this.transform(member)
|
|
300
|
+
if (transformed) base = `${base}.and(${transformed})`
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return base
|
|
304
|
+
},
|
|
305
|
+
...options.nodes,
|
|
306
|
+
},
|
|
307
|
+
print(node) {
|
|
308
|
+
const { keysToOmit } = this.options
|
|
309
|
+
|
|
310
|
+
let base = this.transform(node)
|
|
311
|
+
if (!base) return null
|
|
312
|
+
|
|
313
|
+
const meta = ast.syncSchemaRef(node)
|
|
314
|
+
|
|
315
|
+
if (keysToOmit?.length && meta.primitive === 'object' && !(meta.type === 'union' && meta.discriminatorPropertyName)) {
|
|
316
|
+
// Mirror printerTs `nonNullable: true`: when omitting keys, the resulting
|
|
317
|
+
// schema is a new non-nullable object type — skip optional/nullable/nullish.
|
|
318
|
+
// Discriminated unions (z.discriminatedUnion) do not support .omit(), so skip them.
|
|
319
|
+
|
|
320
|
+
// If this is a lazy reference, apply omit inside the lazy function
|
|
321
|
+
const lazyMatch = base.match(/^z\.lazy\(\(\)\s*=>\s*(.+)\)$/)
|
|
322
|
+
if (lazyMatch) {
|
|
323
|
+
base = `z.lazy(() => ${lazyMatch[1]}.omit({ ${keysToOmit.map((k: string) => `"${k}": true`).join(', ')} }))`
|
|
324
|
+
} else {
|
|
325
|
+
base = `${base}.omit({ ${keysToOmit.map((k: string) => `"${k}": true`).join(', ')} })`
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return applyModifiers({
|
|
330
|
+
value: base,
|
|
331
|
+
nullable: meta.nullable,
|
|
332
|
+
optional: meta.optional,
|
|
333
|
+
nullish: meta.nullish,
|
|
334
|
+
defaultValue: meta.default,
|
|
335
|
+
description: meta.description,
|
|
336
|
+
})
|
|
337
|
+
},
|
|
338
|
+
}
|
|
339
|
+
})
|