@gabrielbryk/json-schema-to-zod 2.12.0 → 2.13.0
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/.github/RELEASE_SETUP.md +120 -0
- package/.github/TOOLING_GUIDE.md +169 -0
- package/.github/dependabot.yml +52 -0
- package/.github/workflows/ci.yml +33 -0
- package/.github/workflows/release.yml +12 -4
- package/.github/workflows/security.yml +40 -0
- package/.husky/commit-msg +1 -0
- package/.husky/pre-commit +1 -0
- package/.lintstagedrc.json +3 -0
- package/.prettierrc +20 -0
- package/AGENTS.md +7 -0
- package/CHANGELOG.md +13 -4
- package/README.md +9 -9
- package/commitlint.config.js +24 -0
- package/createIndex.ts +4 -4
- package/dist/cli.js +3 -4
- package/dist/core/analyzeSchema.js +28 -5
- package/dist/core/emitZod.js +11 -4
- package/dist/generators/generateBundle.js +67 -92
- package/dist/parsers/parseAllOf.js +11 -12
- package/dist/parsers/parseAnyOf.js +2 -2
- package/dist/parsers/parseArray.js +38 -12
- package/dist/parsers/parseMultipleType.js +2 -2
- package/dist/parsers/parseNumber.js +44 -102
- package/dist/parsers/parseObject.js +138 -393
- package/dist/parsers/parseOneOf.js +57 -100
- package/dist/parsers/parseSchema.js +132 -55
- package/dist/parsers/parseSimpleDiscriminatedOneOf.js +2 -2
- package/dist/parsers/parseString.js +113 -253
- package/dist/types/Types.d.ts +22 -1
- package/dist/types/core/analyzeSchema.d.ts +1 -0
- package/dist/types/generators/generateBundle.d.ts +1 -1
- package/dist/utils/cliTools.js +1 -2
- package/dist/utils/esmEmitter.js +6 -2
- package/dist/utils/extractInlineObject.js +1 -3
- package/dist/utils/jsdocs.js +1 -4
- package/dist/utils/liftInlineObjects.js +76 -15
- package/dist/utils/resolveRef.js +35 -10
- package/dist/utils/schemaRepresentation.js +35 -66
- package/dist/zodToJsonSchema.js +1 -2
- package/docs/IMPROVEMENT-PLAN.md +30 -12
- package/docs/ZOD-V4-RECURSIVE-TYPE-LIMITATIONS.md +70 -25
- package/docs/proposals/allof-required-merging.md +10 -4
- package/docs/proposals/bundle-refactor.md +10 -4
- package/docs/proposals/discriminated-union-with-default.md +18 -14
- package/docs/proposals/inline-object-lifting.md +15 -5
- package/docs/proposals/ref-anchor-support.md +11 -0
- package/output.txt +67 -0
- package/package.json +18 -5
- package/scripts/generateWorkflowSchema.ts +5 -14
- package/scripts/regenerate_bundle.ts +25 -0
- package/tsc_output.txt +542 -0
- package/tsc_output_2.txt +489 -0
|
@@ -1,342 +1,139 @@
|
|
|
1
1
|
import { parseAnyOf } from "./parseAnyOf.js";
|
|
2
2
|
import { parseOneOf } from "./parseOneOf.js";
|
|
3
|
-
import {
|
|
4
|
-
import { parseAllOf } from "./parseAllOf.js";
|
|
5
|
-
import { parseIfThenElse } from "./parseIfThenElse.js";
|
|
3
|
+
import { parseSchema } from "./parseSchema.js";
|
|
6
4
|
import { addJsdocs } from "../utils/jsdocs.js";
|
|
7
|
-
import { anyOrUnknown } from "../utils/anyOrUnknown.js";
|
|
8
|
-
import { containsRecursiveRef, inferTypeFromExpression } from "../utils/schemaRepresentation.js";
|
|
9
5
|
export function parseObject(objectSchema, refs) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const parentRequired = Array.isArray(objectSchema.required) ? objectSchema.required : [];
|
|
17
|
-
const allOfRequired = its.an.allOf(objectSchema)
|
|
18
|
-
? objectSchema.allOf.flatMap((member) => {
|
|
19
|
-
if (typeof member !== "object" || member === null)
|
|
20
|
-
return [];
|
|
21
|
-
const req = member.required;
|
|
22
|
-
return Array.isArray(req) ? req : [];
|
|
23
|
-
})
|
|
24
|
-
: [];
|
|
25
|
-
const combinedAllOfRequired = [...new Set([...parentRequired, ...allOfRequired])];
|
|
26
|
-
// Helper to add type: "object" to composition members that have properties but no explicit type
|
|
27
|
-
const addObjectType = (members) => members.map((x) => typeof x === "object" &&
|
|
28
|
-
x !== null &&
|
|
29
|
-
!x.type &&
|
|
30
|
-
(x.properties || x.additionalProperties || x.patternProperties)
|
|
31
|
-
? { ...x, type: "object" }
|
|
32
|
-
: x);
|
|
33
|
-
const addObjectTypeAndMergeRequired = (members) => members.map((x) => {
|
|
34
|
-
if (typeof x !== "object" || x === null)
|
|
35
|
-
return x;
|
|
36
|
-
let normalized = x;
|
|
37
|
-
const hasShape = normalized.properties || normalized.additionalProperties || normalized.patternProperties;
|
|
38
|
-
if (hasShape && !normalized.type) {
|
|
39
|
-
normalized = { ...normalized, type: "object" };
|
|
40
|
-
}
|
|
41
|
-
if (combinedAllOfRequired.length &&
|
|
42
|
-
normalized.properties &&
|
|
43
|
-
Object.keys(normalized.properties).length) {
|
|
44
|
-
const memberRequired = Array.isArray(normalized.required) ? normalized.required : [];
|
|
45
|
-
const mergedRequired = Array.from(new Set([
|
|
46
|
-
...memberRequired,
|
|
47
|
-
...combinedAllOfRequired.filter((key) => Object.prototype.hasOwnProperty.call(normalized.properties, key)),
|
|
48
|
-
]));
|
|
49
|
-
if (mergedRequired.length) {
|
|
50
|
-
normalized = { ...normalized, required: mergedRequired };
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return normalized;
|
|
54
|
-
});
|
|
55
|
-
// If only allOf, delegate to parseAllOf
|
|
56
|
-
if (hasNoDirectSchema && its.an.allOf(objectSchema) && !its.an.anyOf(objectSchema) && !its.a.oneOf(objectSchema) && !its.a.conditional(objectSchema)) {
|
|
57
|
-
return parseAllOf({ ...objectSchema, allOf: addObjectTypeAndMergeRequired(objectSchema.allOf) }, refs);
|
|
58
|
-
}
|
|
59
|
-
// If only anyOf, delegate to parseAnyOf
|
|
60
|
-
if (hasNoDirectSchema && its.an.anyOf(objectSchema) && !its.an.allOf(objectSchema) && !its.a.oneOf(objectSchema) && !its.a.conditional(objectSchema)) {
|
|
61
|
-
return parseAnyOf({ ...objectSchema, anyOf: addObjectType(objectSchema.anyOf) }, refs);
|
|
62
|
-
}
|
|
63
|
-
// If only oneOf, delegate to parseOneOf
|
|
64
|
-
if (hasNoDirectSchema && its.a.oneOf(objectSchema) && !its.an.allOf(objectSchema) && !its.an.anyOf(objectSchema) && !its.a.conditional(objectSchema)) {
|
|
65
|
-
return parseOneOf({ ...objectSchema, oneOf: addObjectType(objectSchema.oneOf) }, refs);
|
|
66
|
-
}
|
|
67
|
-
let properties = undefined;
|
|
68
|
-
// Track property types for building proper object type annotations
|
|
6
|
+
const explicitProps = objectSchema.properties ? Object.keys(objectSchema.properties) : [];
|
|
7
|
+
const requiredProps = Array.isArray(objectSchema.required) ? objectSchema.required : [];
|
|
8
|
+
const allProps = [...new Set([...explicitProps, ...requiredProps])];
|
|
9
|
+
const hasProperties = allProps.length > 0;
|
|
10
|
+
// 1. Process Properties (Base Object)
|
|
11
|
+
let baseObjectExpr = "z.object({";
|
|
69
12
|
const propertyTypes = [];
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const valueWithOptional = optional
|
|
89
|
-
? `${parsedProp.expression}.optional()`
|
|
90
|
-
: parsedProp.expression;
|
|
91
|
-
// Calculate the type for getters (needed for recursive type inference)
|
|
92
|
-
const valueType = optional
|
|
93
|
-
? `z.ZodOptional<${parsedProp.type}>`
|
|
94
|
-
: parsedProp.type;
|
|
95
|
-
// Track the property type for building the object type
|
|
96
|
-
propertyTypes.push({ key, type: valueType });
|
|
97
|
-
const useGetter = shouldUseGetter(valueWithOptional, refs);
|
|
98
|
-
let result = useGetter
|
|
99
|
-
// Type annotation on getter is required for recursive type inference in unions
|
|
100
|
-
? `get ${JSON.stringify(key)}(): ${valueType} { return ${valueWithOptional} }`
|
|
101
|
-
: `${JSON.stringify(key)}: ${valueWithOptional}`;
|
|
102
|
-
if (refs.withJsdocs && typeof propSchema === "object") {
|
|
103
|
-
result = addJsdocs(propSchema, result);
|
|
104
|
-
}
|
|
105
|
-
return result;
|
|
106
|
-
})
|
|
107
|
-
.join(", ");
|
|
108
|
-
properties += " })";
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
const additionalProperties = objectSchema.additionalProperties !== undefined
|
|
112
|
-
? parseSchema(objectSchema.additionalProperties, {
|
|
113
|
-
...refs,
|
|
114
|
-
path: [...refs.path, "additionalProperties"],
|
|
115
|
-
})
|
|
116
|
-
: undefined;
|
|
117
|
-
const unevaluated = objectSchema.unevaluatedProperties;
|
|
118
|
-
const definedPropertyKeys = objectSchema.properties ? Object.keys(objectSchema.properties) : [];
|
|
119
|
-
const missingRequiredKeys = Array.isArray(objectSchema.required)
|
|
120
|
-
? objectSchema.required.filter((key) => !definedPropertyKeys.includes(key))
|
|
121
|
-
: [];
|
|
122
|
-
let patternProperties = undefined;
|
|
123
|
-
if (objectSchema.patternProperties) {
|
|
124
|
-
const parsedPatternProperties = Object.fromEntries(Object.entries(objectSchema.patternProperties).map(([key, value]) => {
|
|
125
|
-
return [
|
|
126
|
-
key,
|
|
127
|
-
parseSchema(value, {
|
|
128
|
-
...refs,
|
|
129
|
-
path: [...refs.path, "patternProperties", key],
|
|
130
|
-
}),
|
|
131
|
-
];
|
|
132
|
-
}, {}));
|
|
133
|
-
// Helper to get expressions from parsed pattern properties
|
|
134
|
-
const patternExprs = Object.values(parsedPatternProperties).map(r => r.expression);
|
|
135
|
-
patternProperties = "";
|
|
136
|
-
if (properties) {
|
|
137
|
-
if (additionalProperties) {
|
|
138
|
-
patternProperties += `.catchall(z.union([${[
|
|
139
|
-
...patternExprs,
|
|
140
|
-
additionalProperties.expression,
|
|
141
|
-
].join(", ")}]))`;
|
|
142
|
-
}
|
|
143
|
-
else if (Object.keys(parsedPatternProperties).length > 1) {
|
|
144
|
-
patternProperties += `.catchall(z.union([${patternExprs.join(", ")}]))`;
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
patternProperties += `.catchall(${patternExprs.join("")})`;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
else {
|
|
151
|
-
if (additionalProperties) {
|
|
152
|
-
patternProperties += `z.record(z.string(), z.union([${[
|
|
153
|
-
...patternExprs,
|
|
154
|
-
additionalProperties.expression,
|
|
155
|
-
].join(", ")}]))`;
|
|
156
|
-
}
|
|
157
|
-
else if (Object.keys(parsedPatternProperties).length > 1) {
|
|
158
|
-
patternProperties += `z.record(z.string(), z.union([${patternExprs.join(", ")}]))`;
|
|
13
|
+
if (hasProperties) {
|
|
14
|
+
baseObjectExpr += allProps
|
|
15
|
+
.map((key) => {
|
|
16
|
+
const propSchema = objectSchema.properties?.[key];
|
|
17
|
+
const parsedProp = propSchema
|
|
18
|
+
? parseSchema(propSchema, { ...refs, path: [...refs.path, "properties", key] })
|
|
19
|
+
: { expression: "z.any()", type: "z.ZodAny" };
|
|
20
|
+
const hasDefault = typeof propSchema === "object" && propSchema.default !== undefined;
|
|
21
|
+
// Check "required" array from parent
|
|
22
|
+
const isRequired = Array.isArray(objectSchema.required)
|
|
23
|
+
? objectSchema.required.includes(key)
|
|
24
|
+
: typeof propSchema === "object" && propSchema.required === true;
|
|
25
|
+
const isOptional = !hasDefault && !isRequired;
|
|
26
|
+
let valueExpr = parsedProp.expression;
|
|
27
|
+
let valueType = parsedProp.type;
|
|
28
|
+
if (isOptional) {
|
|
29
|
+
valueExpr = `${parsedProp.expression}.exactOptional()`;
|
|
30
|
+
valueType = `z.ZodExactOptional<${parsedProp.type}>`;
|
|
159
31
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
patternProperties += ".superRefine((value, ctx) => {\n";
|
|
165
|
-
patternProperties += "for (const key in value) {\n";
|
|
166
|
-
if (additionalProperties) {
|
|
167
|
-
if (objectSchema.properties) {
|
|
168
|
-
patternProperties += `let evaluated = [${Object.keys(objectSchema.properties)
|
|
169
|
-
.map((key) => JSON.stringify(key))
|
|
170
|
-
.join(", ")}].includes(key)\n`;
|
|
171
|
-
}
|
|
172
|
-
else {
|
|
173
|
-
patternProperties += `let evaluated = false\n`;
|
|
32
|
+
propertyTypes.push({ key, type: valueType });
|
|
33
|
+
if (refs.withJsdocs && typeof propSchema === "object") {
|
|
34
|
+
valueExpr = addJsdocs(propSchema, valueExpr);
|
|
174
35
|
}
|
|
36
|
+
return `${JSON.stringify(key)}: ${valueExpr}`;
|
|
37
|
+
})
|
|
38
|
+
.join(", ");
|
|
39
|
+
}
|
|
40
|
+
baseObjectExpr += "})";
|
|
41
|
+
const additionalProps = objectSchema.additionalProperties;
|
|
42
|
+
let baseObjectModified = baseObjectExpr;
|
|
43
|
+
const patternProps = objectSchema.patternProperties || {};
|
|
44
|
+
const patterns = Object.keys(patternProps);
|
|
45
|
+
const hasPattern = patterns.length > 0;
|
|
46
|
+
// Logic to determine if we need manual handling of additionalProperties
|
|
47
|
+
// This is required if we have patternProperties AND additionalProperties is restrictive (false or schema)
|
|
48
|
+
// because Zod's .catchall() or .strict() would incorrectly rejeect/validate pattern-matched keys.
|
|
49
|
+
const isAdPropsRestrictive = additionalProps === false || (additionalProps && typeof additionalProps === "object");
|
|
50
|
+
const manualAdditionalProps = hasPattern && isAdPropsRestrictive;
|
|
51
|
+
let addPropsSchema;
|
|
52
|
+
if (manualAdditionalProps) {
|
|
53
|
+
baseObjectModified = baseObjectExpr.replace(/^z\.object\(/, "z.looseObject(");
|
|
54
|
+
if (typeof additionalProps === "object") {
|
|
55
|
+
addPropsSchema = parseSchema(additionalProps, {
|
|
56
|
+
...refs,
|
|
57
|
+
path: [...refs.path, "additionalProperties"],
|
|
58
|
+
});
|
|
175
59
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
patternProperties += "evaluated = true\n";
|
|
181
|
-
}
|
|
182
|
-
patternProperties +=
|
|
183
|
-
"const result = " +
|
|
184
|
-
parsedPatternProperties[key].expression +
|
|
185
|
-
".safeParse(value[key])\n";
|
|
186
|
-
patternProperties += "if (!result.success) {\n";
|
|
187
|
-
patternProperties += `ctx.addIssue({
|
|
188
|
-
path: [...(ctx.path ?? []), key],
|
|
189
|
-
code: 'custom',
|
|
190
|
-
message: \`Invalid input: Key matching regex /\${key}/ must match schema\`,
|
|
191
|
-
params: {
|
|
192
|
-
issues: result.error.issues
|
|
193
|
-
}
|
|
194
|
-
})\n`;
|
|
195
|
-
patternProperties += "}\n";
|
|
196
|
-
patternProperties += "}\n";
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
if (additionalProps === false) {
|
|
63
|
+
baseObjectModified = baseObjectExpr.replace(/^z\.object\(/, "z.strictObject(");
|
|
197
64
|
}
|
|
198
|
-
if (
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
code: 'custom',
|
|
206
|
-
message: \`Invalid input: must match catchall schema\`,
|
|
207
|
-
params: {
|
|
208
|
-
issues: result.error.issues
|
|
209
|
-
}
|
|
210
|
-
})\n`;
|
|
211
|
-
patternProperties += "}\n";
|
|
212
|
-
patternProperties += "}\n";
|
|
65
|
+
else if (additionalProps && typeof additionalProps === "object") {
|
|
66
|
+
addPropsSchema = parseSchema(additionalProps, {
|
|
67
|
+
...refs,
|
|
68
|
+
path: [...refs.path, "additionalProperties"],
|
|
69
|
+
});
|
|
70
|
+
baseObjectModified = baseObjectExpr.replace(/^z\.object\(/, "z.looseObject(");
|
|
71
|
+
baseObjectModified += `.catchall(${addPropsSchema.expression})`;
|
|
213
72
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
// Store original patternProperties in meta for JSON Schema round-trip
|
|
217
|
-
if (refs.preserveJsonSchemaForRoundTrip) {
|
|
218
|
-
const patternPropsJson = JSON.stringify(Object.fromEntries(Object.entries(objectSchema.patternProperties).map(([pattern, schema]) => [
|
|
219
|
-
pattern,
|
|
220
|
-
schema
|
|
221
|
-
])));
|
|
222
|
-
patternProperties += `.meta({ __jsonSchema: { patternProperties: ${patternPropsJson} } })`;
|
|
73
|
+
else {
|
|
74
|
+
baseObjectModified = baseObjectExpr.replace(/^z\.object\(/, "z.looseObject(");
|
|
223
75
|
}
|
|
224
76
|
}
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
: properties
|
|
245
|
-
: patternProperties
|
|
246
|
-
? patternProperties
|
|
247
|
-
: additionalProperties
|
|
248
|
-
? `z.record(z.string(), ${additionalProperties.expression})`
|
|
249
|
-
// If we have composition keywords, start with empty object instead of z.record()
|
|
250
|
-
// The composition will provide the actual schema via .and()
|
|
251
|
-
: hasCompositionKeywords
|
|
252
|
-
? "z.object({})"
|
|
253
|
-
// No constraints = any object. Use z.record() which is cleaner than z.object({}).catchall()
|
|
254
|
-
: `z.record(z.string(), ${fallback.expression})`;
|
|
255
|
-
if (unevaluated === false && properties && !hasCompositionKeywords) {
|
|
256
|
-
output += ".strict()";
|
|
257
|
-
}
|
|
258
|
-
else if (unevaluated && typeof unevaluated !== 'boolean') {
|
|
259
|
-
const unevaluatedSchema = parseSchema(unevaluated, {
|
|
260
|
-
...refs,
|
|
261
|
-
path: [...refs.path, "unevaluatedProperties"],
|
|
262
|
-
});
|
|
263
|
-
const knownKeys = objectSchema.properties ? Object.keys(objectSchema.properties) : [];
|
|
264
|
-
const patterns = objectSchema.patternProperties
|
|
265
|
-
? Object.keys(objectSchema.patternProperties).map((p) => new RegExp(p))
|
|
266
|
-
: [];
|
|
267
|
-
output += `.superRefine((value, ctx) => {
|
|
77
|
+
// 3. Handle patternProperties using Intersection with z.looseRecord
|
|
78
|
+
let finalExpr = baseObjectModified;
|
|
79
|
+
const intersectionTypes = [];
|
|
80
|
+
if (hasPattern) {
|
|
81
|
+
for (const [pattern, schema] of Object.entries(patternProps)) {
|
|
82
|
+
const validSchema = parseSchema(schema, {
|
|
83
|
+
...refs,
|
|
84
|
+
path: [...refs.path, "patternProperties", pattern],
|
|
85
|
+
});
|
|
86
|
+
const keySchema = `z.string().regex(new RegExp(${JSON.stringify(pattern)}))`;
|
|
87
|
+
const recordExpr = `z.looseRecord(${keySchema}, ${validSchema.expression})`;
|
|
88
|
+
finalExpr = `z.intersection(${finalExpr}, ${recordExpr})`;
|
|
89
|
+
intersectionTypes.push(`z.ZodRecord<z.ZodString, ${validSchema.type}>`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// 3b. Add manual additionalProperties check if needed
|
|
93
|
+
if (manualAdditionalProps) {
|
|
94
|
+
const definedProps = objectSchema.properties ? Object.keys(objectSchema.properties) : [];
|
|
95
|
+
finalExpr += `.superRefine((value, ctx) => {
|
|
268
96
|
for (const key in value) {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if (
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
97
|
+
if (${JSON.stringify(definedProps)}.includes(key)) continue;
|
|
98
|
+
let matched = false;
|
|
99
|
+
${patterns.map((p) => `if (new RegExp(${JSON.stringify(p)}).test(key)) matched = true;`).join("\n ")}
|
|
100
|
+
if (matched) continue;
|
|
101
|
+
|
|
102
|
+
${additionalProps === false
|
|
103
|
+
? `ctx.addIssue({ code: "custom", message: "Invalid key/Strict", path: [...ctx.path, key] });`
|
|
104
|
+
: `const result = ${addPropsSchema.expression}.safeParse(value[key]);
|
|
105
|
+
if (!result.success) {
|
|
106
|
+
ctx.addIssue({ path: [...ctx.path, key], code: "custom", message: "Invalid additional property", params: { issues: result.error.issues } });
|
|
107
|
+
}`}
|
|
277
108
|
}
|
|
278
109
|
})`;
|
|
279
110
|
}
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
output += `.and(${anyOfResult.expression})`;
|
|
293
|
-
intersectionTypes.push(anyOfResult.type);
|
|
294
|
-
}
|
|
295
|
-
if (its.a.oneOf(objectSchema)) {
|
|
296
|
-
const oneOfResult = parseOneOf({
|
|
297
|
-
...objectSchema,
|
|
298
|
-
oneOf: objectSchema.oneOf.map((x) => typeof x === "object" &&
|
|
299
|
-
x !== null &&
|
|
300
|
-
!x.type &&
|
|
301
|
-
(x.properties || x.additionalProperties || x.patternProperties)
|
|
302
|
-
? { ...x, type: "object" }
|
|
303
|
-
: x),
|
|
304
|
-
}, refs);
|
|
305
|
-
// Check if this is a refinement-only result (required fields validation)
|
|
306
|
-
// If so, apply superRefine directly instead of creating an intersection
|
|
307
|
-
const resultWithRefinement = oneOfResult;
|
|
308
|
-
if (resultWithRefinement.isRefinementOnly && resultWithRefinement.refinementBody) {
|
|
309
|
-
output += `.superRefine(${resultWithRefinement.refinementBody})`;
|
|
310
|
-
// No intersection type needed - superRefine doesn't change the type
|
|
311
|
-
}
|
|
312
|
-
else {
|
|
313
|
-
output += `.and(${oneOfResult.expression})`;
|
|
314
|
-
intersectionTypes.push(oneOfResult.type);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
if (its.an.allOf(objectSchema)) {
|
|
318
|
-
const allOfResult = parseAllOf({
|
|
319
|
-
...objectSchema,
|
|
320
|
-
allOf: addObjectTypeAndMergeRequired(objectSchema.allOf),
|
|
321
|
-
}, refs);
|
|
322
|
-
output += `.and(${allOfResult.expression})`;
|
|
323
|
-
intersectionTypes.push(allOfResult.type);
|
|
111
|
+
// 4. Handle composition (allOf, oneOf, anyOf) via Intersection
|
|
112
|
+
if (objectSchema.allOf) {
|
|
113
|
+
// Cast because we checked it exists
|
|
114
|
+
const schemaWithAllOf = objectSchema;
|
|
115
|
+
// Note: parseAllOf usually handles the whole schema logic, filtering properties.
|
|
116
|
+
// But typically allOf implies intersection.
|
|
117
|
+
// If we just use simple intersection:
|
|
118
|
+
schemaWithAllOf.allOf.forEach((s, i) => {
|
|
119
|
+
const res = parseSchema(s, { ...refs, path: [...refs.path, "allOf", i] });
|
|
120
|
+
finalExpr = `z.intersection(${finalExpr}, ${res.expression})`;
|
|
121
|
+
intersectionTypes.push(res.type);
|
|
122
|
+
});
|
|
324
123
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
intersectionTypes.push(
|
|
124
|
+
if (objectSchema.oneOf) {
|
|
125
|
+
const schemaWithOneOf = objectSchema;
|
|
126
|
+
const res = parseOneOf(schemaWithOneOf, refs);
|
|
127
|
+
finalExpr = `z.intersection(${finalExpr}, ${res.expression})`;
|
|
128
|
+
intersectionTypes.push(res.type);
|
|
330
129
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
.join(" ");
|
|
337
|
-
output += `.superRefine((value, ctx) => { if (value && typeof value === "object") { ${checks} } })`;
|
|
130
|
+
if (objectSchema.anyOf) {
|
|
131
|
+
const schemaWithAnyOf = objectSchema;
|
|
132
|
+
const res = parseAnyOf(schemaWithAnyOf, refs);
|
|
133
|
+
finalExpr = `z.intersection(${finalExpr}, ${res.expression})`;
|
|
134
|
+
intersectionTypes.push(res.type);
|
|
338
135
|
}
|
|
339
|
-
// propertyNames
|
|
136
|
+
// 5. propertyNames, unevaluatedProperties, dependentSchemas etc.
|
|
340
137
|
if (objectSchema.propertyNames) {
|
|
341
138
|
const normalizedPropNames = typeof objectSchema.propertyNames === "object" &&
|
|
342
139
|
objectSchema.propertyNames !== null &&
|
|
@@ -348,9 +145,9 @@ export function parseObject(objectSchema, refs) {
|
|
|
348
145
|
...refs,
|
|
349
146
|
path: [...refs.path, "propertyNames"],
|
|
350
147
|
});
|
|
351
|
-
|
|
148
|
+
finalExpr += `.superRefine((value, ctx) => {
|
|
352
149
|
for (const key in value) {
|
|
353
|
-
const result = ${propNameSchema}.safeParse(key);
|
|
150
|
+
const result = ${propNameSchema.expression}.safeParse(key);
|
|
354
151
|
if (!result.success) {
|
|
355
152
|
ctx.addIssue({
|
|
356
153
|
path: [key],
|
|
@@ -366,12 +163,15 @@ export function parseObject(objectSchema, refs) {
|
|
|
366
163
|
if (objectSchema.dependentSchemas && typeof objectSchema.dependentSchemas === "object") {
|
|
367
164
|
const entries = Object.entries(objectSchema.dependentSchemas);
|
|
368
165
|
if (entries.length) {
|
|
369
|
-
|
|
166
|
+
finalExpr += `.superRefine((obj, ctx) => {
|
|
370
167
|
${entries
|
|
371
168
|
.map(([key, schema]) => {
|
|
372
|
-
const parsed = parseSchema(schema, {
|
|
169
|
+
const parsed = parseSchema(schema, {
|
|
170
|
+
...refs,
|
|
171
|
+
path: [...refs.path, "dependentSchemas", key],
|
|
172
|
+
});
|
|
373
173
|
return `if (Object.prototype.hasOwnProperty.call(obj, ${JSON.stringify(key)})) {
|
|
374
|
-
const result = ${parsed}.safeParse(obj);
|
|
174
|
+
const result = ${parsed.expression}.safeParse(obj);
|
|
375
175
|
if (!result.success) {
|
|
376
176
|
ctx.addIssue({ code: "custom", message: ${objectSchema.errorMessage?.dependentSchemas ?? JSON.stringify("Dependent schema failed")}, path: [], params: { issues: result.error.issues } });
|
|
377
177
|
}
|
|
@@ -385,9 +185,9 @@ export function parseObject(objectSchema, refs) {
|
|
|
385
185
|
if (objectSchema.dependentRequired && typeof objectSchema.dependentRequired === "object") {
|
|
386
186
|
const entries = Object.entries(objectSchema.dependentRequired);
|
|
387
187
|
if (entries.length) {
|
|
388
|
-
const depRequiredMessage = objectSchema.errorMessage
|
|
389
|
-
"Dependent required properties missing";
|
|
390
|
-
|
|
188
|
+
const depRequiredMessage = objectSchema.errorMessage
|
|
189
|
+
?.dependentRequired ?? "Dependent required properties missing";
|
|
190
|
+
finalExpr += `.superRefine((obj, ctx) => {
|
|
391
191
|
${entries
|
|
392
192
|
.map(([prop, deps]) => {
|
|
393
193
|
const arr = Array.isArray(deps) ? deps : [];
|
|
@@ -406,73 +206,18 @@ export function parseObject(objectSchema, refs) {
|
|
|
406
206
|
})`;
|
|
407
207
|
}
|
|
408
208
|
}
|
|
409
|
-
//
|
|
410
|
-
let type;
|
|
411
|
-
if (propertyTypes.length
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
.map(({ key, type: propType }) => `${JSON.stringify(key)}: ${propType}`)
|
|
415
|
-
.join("; ");
|
|
416
|
-
type = `z.ZodObject<{ ${typeShape} }>`;
|
|
417
|
-
}
|
|
418
|
-
else if (properties === "z.object({})") {
|
|
419
|
-
// Empty object
|
|
420
|
-
type = "z.ZodObject<{}>";
|
|
421
|
-
}
|
|
422
|
-
else {
|
|
423
|
-
// Fallback for complex cases (patternProperties, record, etc.)
|
|
424
|
-
type = inferTypeFromExpression(output);
|
|
425
|
-
}
|
|
426
|
-
// Wrap in intersection types if .and() calls were added
|
|
427
|
-
for (const intersectionType of intersectionTypes) {
|
|
428
|
-
type = `z.ZodIntersection<${type}, ${intersectionType}>`;
|
|
209
|
+
// Calculate Type
|
|
210
|
+
let type = "z.ZodObject<any>";
|
|
211
|
+
if (propertyTypes.length) {
|
|
212
|
+
const shape = propertyTypes.map((p) => `${JSON.stringify(p.key)}: ${p.type}`).join("; ");
|
|
213
|
+
type = `z.ZodObject<{${shape}}>`;
|
|
429
214
|
}
|
|
215
|
+
// If intersections
|
|
216
|
+
intersectionTypes.forEach((t) => {
|
|
217
|
+
type = `z.ZodIntersection<${type}, ${t}>`;
|
|
218
|
+
});
|
|
430
219
|
return {
|
|
431
|
-
expression:
|
|
220
|
+
expression: finalExpr,
|
|
432
221
|
type,
|
|
433
222
|
};
|
|
434
223
|
}
|
|
435
|
-
/**
|
|
436
|
-
* Determines if a property should use getter syntax for recursive references.
|
|
437
|
-
* Getters defer evaluation until access time, which is the Zod v4 recommended
|
|
438
|
-
* approach for handling recursive schemas in object properties.
|
|
439
|
-
*/
|
|
440
|
-
const shouldUseGetter = (parsed, refs) => {
|
|
441
|
-
if (!parsed)
|
|
442
|
-
return false;
|
|
443
|
-
// Check for z.lazy() - these should use getters
|
|
444
|
-
if (parsed.includes("z.lazy("))
|
|
445
|
-
return true;
|
|
446
|
-
// Check for direct self-recursion (expression contains the current schema name)
|
|
447
|
-
// This handles cases like generateSchemaBundle where the schema name is different
|
|
448
|
-
// from the def name (e.g., NodeSchema vs node)
|
|
449
|
-
if (refs.currentSchemaName) {
|
|
450
|
-
const selfRefPattern = new RegExp(`\\b${refs.currentSchemaName}\\b`);
|
|
451
|
-
if (selfRefPattern.test(parsed)) {
|
|
452
|
-
return true;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
// Check for direct recursive references in the same SCC
|
|
456
|
-
if (refs.currentSchemaName && refs.cycleRefNames && refs.cycleComponentByName) {
|
|
457
|
-
const cycleRefNames = refs.cycleRefNames;
|
|
458
|
-
const cycleComponentByName = refs.cycleComponentByName;
|
|
459
|
-
const refNameArray = Array.from(cycleRefNames);
|
|
460
|
-
// Check if expression contains a reference to a cycle member in the same component
|
|
461
|
-
if (containsRecursiveRef(parsed, cycleRefNames)) {
|
|
462
|
-
const currentComponent = cycleComponentByName.get(refs.currentSchemaName);
|
|
463
|
-
if (currentComponent !== undefined) {
|
|
464
|
-
for (let i = 0; i < refNameArray.length; i++) {
|
|
465
|
-
const refName = refNameArray[i];
|
|
466
|
-
const pattern = new RegExp(`\\b${refName}\\b`);
|
|
467
|
-
if (pattern.test(parsed)) {
|
|
468
|
-
const refComponent = cycleComponentByName.get(refName);
|
|
469
|
-
if (refComponent === currentComponent) {
|
|
470
|
-
return true;
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
return false;
|
|
478
|
-
};
|