@gabrielbryk/json-schema-to-zod 2.12.1 → 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 +7 -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 +136 -443
- package/dist/parsers/parseOneOf.js +57 -110
- 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,394 +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
|
-
const collectKnownPropertyKeys = (schema) => {
|
|
10
|
-
const keys = new Set();
|
|
11
|
-
const visit = (node) => {
|
|
12
|
-
if (typeof node !== "object" || node === null)
|
|
13
|
-
return;
|
|
14
|
-
const obj = node;
|
|
15
|
-
if (obj.properties && typeof obj.properties === "object") {
|
|
16
|
-
Object.keys(obj.properties).forEach((key) => keys.add(key));
|
|
17
|
-
}
|
|
18
|
-
};
|
|
19
|
-
visit(schema);
|
|
20
|
-
if (Array.isArray(schema.oneOf))
|
|
21
|
-
schema.oneOf.forEach(visit);
|
|
22
|
-
if (Array.isArray(schema.anyOf))
|
|
23
|
-
schema.anyOf.forEach(visit);
|
|
24
|
-
if (Array.isArray(schema.allOf))
|
|
25
|
-
schema.allOf.forEach(visit);
|
|
26
|
-
return Array.from(keys);
|
|
27
|
-
};
|
|
28
5
|
export function parseObject(objectSchema, refs) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const parentRequired = Array.isArray(objectSchema.required) ? objectSchema.required : [];
|
|
36
|
-
const allOfRequired = its.an.allOf(objectSchema)
|
|
37
|
-
? objectSchema.allOf.flatMap((member) => {
|
|
38
|
-
if (typeof member !== "object" || member === null)
|
|
39
|
-
return [];
|
|
40
|
-
const req = member.required;
|
|
41
|
-
return Array.isArray(req) ? req : [];
|
|
42
|
-
})
|
|
43
|
-
: [];
|
|
44
|
-
const combinedAllOfRequired = [...new Set([...parentRequired, ...allOfRequired])];
|
|
45
|
-
// Helper to add type: "object" to composition members that have properties but no explicit type
|
|
46
|
-
const addObjectType = (members) => members.map((x) => typeof x === "object" &&
|
|
47
|
-
x !== null &&
|
|
48
|
-
!x.type &&
|
|
49
|
-
(x.properties || x.additionalProperties || x.patternProperties)
|
|
50
|
-
? { ...x, type: "object" }
|
|
51
|
-
: x);
|
|
52
|
-
const addObjectTypeAndMergeRequired = (members) => members.map((x) => {
|
|
53
|
-
if (typeof x !== "object" || x === null)
|
|
54
|
-
return x;
|
|
55
|
-
let normalized = x;
|
|
56
|
-
const hasShape = normalized.properties || normalized.additionalProperties || normalized.patternProperties;
|
|
57
|
-
if (hasShape && !normalized.type) {
|
|
58
|
-
normalized = { ...normalized, type: "object" };
|
|
59
|
-
}
|
|
60
|
-
if (combinedAllOfRequired.length &&
|
|
61
|
-
normalized.properties &&
|
|
62
|
-
Object.keys(normalized.properties).length) {
|
|
63
|
-
const memberRequired = Array.isArray(normalized.required) ? normalized.required : [];
|
|
64
|
-
const mergedRequired = Array.from(new Set([
|
|
65
|
-
...memberRequired,
|
|
66
|
-
...combinedAllOfRequired.filter((key) => Object.prototype.hasOwnProperty.call(normalized.properties, key)),
|
|
67
|
-
]));
|
|
68
|
-
if (mergedRequired.length) {
|
|
69
|
-
normalized = { ...normalized, required: mergedRequired };
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return normalized;
|
|
73
|
-
});
|
|
74
|
-
// If only allOf, delegate to parseAllOf
|
|
75
|
-
if (hasNoDirectSchema && its.an.allOf(objectSchema) && !its.an.anyOf(objectSchema) && !its.a.oneOf(objectSchema) && !its.a.conditional(objectSchema)) {
|
|
76
|
-
return parseAllOf({ ...objectSchema, allOf: addObjectTypeAndMergeRequired(objectSchema.allOf) }, refs);
|
|
77
|
-
}
|
|
78
|
-
// If only anyOf, delegate to parseAnyOf
|
|
79
|
-
if (hasNoDirectSchema && its.an.anyOf(objectSchema) && !its.an.allOf(objectSchema) && !its.a.oneOf(objectSchema) && !its.a.conditional(objectSchema)) {
|
|
80
|
-
return parseAnyOf({ ...objectSchema, anyOf: addObjectType(objectSchema.anyOf) }, refs);
|
|
81
|
-
}
|
|
82
|
-
// If only oneOf, delegate to parseOneOf
|
|
83
|
-
if (hasNoDirectSchema && its.a.oneOf(objectSchema) && !its.an.allOf(objectSchema) && !its.an.anyOf(objectSchema) && !its.a.conditional(objectSchema)) {
|
|
84
|
-
return parseOneOf({ ...objectSchema, oneOf: addObjectType(objectSchema.oneOf) }, refs);
|
|
85
|
-
}
|
|
86
|
-
let properties = undefined;
|
|
87
|
-
// 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({";
|
|
88
12
|
const propertyTypes = [];
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const valueWithOptional = optional
|
|
108
|
-
? `${parsedProp.expression}.optional()`
|
|
109
|
-
: parsedProp.expression;
|
|
110
|
-
// Calculate the type for getters (needed for recursive type inference)
|
|
111
|
-
const valueType = optional
|
|
112
|
-
? `z.ZodOptional<${parsedProp.type}>`
|
|
113
|
-
: parsedProp.type;
|
|
114
|
-
// Track the property type for building the object type
|
|
115
|
-
propertyTypes.push({ key, type: valueType });
|
|
116
|
-
const useGetter = shouldUseGetter(valueWithOptional, refs);
|
|
117
|
-
let result = useGetter
|
|
118
|
-
// Type annotation on getter is required for recursive type inference in unions
|
|
119
|
-
? `get ${JSON.stringify(key)}(): ${valueType} { return ${valueWithOptional} }`
|
|
120
|
-
: `${JSON.stringify(key)}: ${valueWithOptional}`;
|
|
121
|
-
if (refs.withJsdocs && typeof propSchema === "object") {
|
|
122
|
-
result = addJsdocs(propSchema, result);
|
|
123
|
-
}
|
|
124
|
-
return result;
|
|
125
|
-
})
|
|
126
|
-
.join(", ");
|
|
127
|
-
properties += " })";
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
const additionalProperties = objectSchema.additionalProperties !== undefined
|
|
131
|
-
? parseSchema(objectSchema.additionalProperties, {
|
|
132
|
-
...refs,
|
|
133
|
-
path: [...refs.path, "additionalProperties"],
|
|
134
|
-
})
|
|
135
|
-
: undefined;
|
|
136
|
-
const unevaluated = objectSchema.unevaluatedProperties;
|
|
137
|
-
const definedPropertyKeys = objectSchema.properties ? Object.keys(objectSchema.properties) : [];
|
|
138
|
-
const missingRequiredKeys = Array.isArray(objectSchema.required)
|
|
139
|
-
? objectSchema.required.filter((key) => !definedPropertyKeys.includes(key))
|
|
140
|
-
: [];
|
|
141
|
-
let patternProperties = undefined;
|
|
142
|
-
if (objectSchema.patternProperties) {
|
|
143
|
-
const parsedPatternProperties = Object.fromEntries(Object.entries(objectSchema.patternProperties).map(([key, value]) => {
|
|
144
|
-
return [
|
|
145
|
-
key,
|
|
146
|
-
parseSchema(value, {
|
|
147
|
-
...refs,
|
|
148
|
-
path: [...refs.path, "patternProperties", key],
|
|
149
|
-
}),
|
|
150
|
-
];
|
|
151
|
-
}, {}));
|
|
152
|
-
// Helper to get expressions from parsed pattern properties
|
|
153
|
-
const patternExprs = Object.values(parsedPatternProperties).map(r => r.expression);
|
|
154
|
-
patternProperties = "";
|
|
155
|
-
if (properties) {
|
|
156
|
-
if (additionalProperties) {
|
|
157
|
-
patternProperties += `.catchall(z.union([${[
|
|
158
|
-
...patternExprs,
|
|
159
|
-
additionalProperties.expression,
|
|
160
|
-
].join(", ")}]))`;
|
|
161
|
-
}
|
|
162
|
-
else if (Object.keys(parsedPatternProperties).length > 1) {
|
|
163
|
-
patternProperties += `.catchall(z.union([${patternExprs.join(", ")}]))`;
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
patternProperties += `.catchall(${patternExprs.join("")})`;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
else {
|
|
170
|
-
if (additionalProperties) {
|
|
171
|
-
patternProperties += `z.record(z.string(), z.union([${[
|
|
172
|
-
...patternExprs,
|
|
173
|
-
additionalProperties.expression,
|
|
174
|
-
].join(", ")}]))`;
|
|
175
|
-
}
|
|
176
|
-
else if (Object.keys(parsedPatternProperties).length > 1) {
|
|
177
|
-
patternProperties += `z.record(z.string(), z.union([${patternExprs.join(", ")}]))`;
|
|
178
|
-
}
|
|
179
|
-
else {
|
|
180
|
-
patternProperties += `z.record(z.string(), ${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}>`;
|
|
181
31
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (additionalProperties) {
|
|
186
|
-
if (objectSchema.properties) {
|
|
187
|
-
patternProperties += `let evaluated = [${Object.keys(objectSchema.properties)
|
|
188
|
-
.map((key) => JSON.stringify(key))
|
|
189
|
-
.join(", ")}].includes(key)\n`;
|
|
190
|
-
}
|
|
191
|
-
else {
|
|
192
|
-
patternProperties += `let evaluated = false\n`;
|
|
32
|
+
propertyTypes.push({ key, type: valueType });
|
|
33
|
+
if (refs.withJsdocs && typeof propSchema === "object") {
|
|
34
|
+
valueExpr = addJsdocs(propSchema, valueExpr);
|
|
193
35
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (additionalProperties) {
|
|
218
|
-
patternProperties += "if (!evaluated) {\n";
|
|
219
|
-
patternProperties +=
|
|
220
|
-
"const result = " + additionalProperties.expression + ".safeParse(value[key])\n";
|
|
221
|
-
patternProperties += "if (!result.success) {\n";
|
|
222
|
-
patternProperties += `ctx.addIssue({
|
|
223
|
-
path: [...(ctx.path ?? []), key],
|
|
224
|
-
code: 'custom',
|
|
225
|
-
message: \`Invalid input: must match catchall schema\`,
|
|
226
|
-
params: {
|
|
227
|
-
issues: result.error.issues
|
|
228
|
-
}
|
|
229
|
-
})\n`;
|
|
230
|
-
patternProperties += "}\n";
|
|
231
|
-
patternProperties += "}\n";
|
|
232
|
-
}
|
|
233
|
-
patternProperties += "}\n";
|
|
234
|
-
patternProperties += "})";
|
|
235
|
-
// Store original patternProperties in meta for JSON Schema round-trip
|
|
236
|
-
if (refs.preserveJsonSchemaForRoundTrip) {
|
|
237
|
-
const patternPropsJson = JSON.stringify(Object.fromEntries(Object.entries(objectSchema.patternProperties).map(([pattern, schema]) => [
|
|
238
|
-
pattern,
|
|
239
|
-
schema
|
|
240
|
-
])));
|
|
241
|
-
patternProperties += `.meta({ __jsonSchema: { patternProperties: ${patternPropsJson} } })`;
|
|
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
|
+
});
|
|
242
59
|
}
|
|
243
60
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const hasCompositionKeywords = its.an.anyOf(objectSchema) || its.a.oneOf(objectSchema) || its.an.allOf(objectSchema) || its.a.conditional(objectSchema);
|
|
248
|
-
// When there are composition keywords (allOf, anyOf, oneOf, if-then-else) but no direct properties,
|
|
249
|
-
// we should NOT default to z.record(z.string(), z.any()) because that would allow any properties.
|
|
250
|
-
// Instead, use z.object({}) and let the .and() call add properties from the composition.
|
|
251
|
-
// This is especially important when unevaluatedProperties: false is set.
|
|
252
|
-
const shouldPassthroughForUnevaluated = unevaluated === false && hasCompositionKeywords;
|
|
253
|
-
const passthroughProperties = shouldPassthroughForUnevaluated && properties && !patternProperties
|
|
254
|
-
? `${properties}.passthrough()`
|
|
255
|
-
: properties;
|
|
256
|
-
const fallback = anyOrUnknown(refs);
|
|
257
|
-
let output;
|
|
258
|
-
if (properties) {
|
|
259
|
-
if (patternProperties) {
|
|
260
|
-
output = properties + patternProperties;
|
|
61
|
+
else {
|
|
62
|
+
if (additionalProps === false) {
|
|
63
|
+
baseObjectModified = baseObjectExpr.replace(/^z\.object\(/, "z.strictObject(");
|
|
261
64
|
}
|
|
262
|
-
else if (
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
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})`;
|
|
270
72
|
}
|
|
271
73
|
else {
|
|
272
|
-
|
|
74
|
+
baseObjectModified = baseObjectExpr.replace(/^z\.object\(/, "z.looseObject(");
|
|
273
75
|
}
|
|
274
76
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
const knownKeys = objectSchema.properties ? Object.keys(objectSchema.properties) : [];
|
|
295
|
-
const patterns = objectSchema.patternProperties
|
|
296
|
-
? Object.keys(objectSchema.patternProperties).map((p) => new RegExp(p))
|
|
297
|
-
: [];
|
|
298
|
-
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) => {
|
|
299
96
|
for (const key in value) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
if (
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
+
}`}
|
|
308
108
|
}
|
|
309
109
|
})`;
|
|
310
110
|
}
|
|
311
|
-
//
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
output += `.and(${anyOfResult.expression})`;
|
|
324
|
-
intersectionTypes.push(anyOfResult.type);
|
|
325
|
-
}
|
|
326
|
-
if (its.a.oneOf(objectSchema)) {
|
|
327
|
-
const oneOfResult = parseOneOf({
|
|
328
|
-
...objectSchema,
|
|
329
|
-
oneOf: objectSchema.oneOf.map((x) => typeof x === "object" &&
|
|
330
|
-
x !== null &&
|
|
331
|
-
!x.type &&
|
|
332
|
-
(x.properties || x.additionalProperties || x.patternProperties)
|
|
333
|
-
? { ...x, type: "object" }
|
|
334
|
-
: x),
|
|
335
|
-
}, refs);
|
|
336
|
-
// Check if this is a refinement-only result (required fields validation)
|
|
337
|
-
// If so, apply superRefine directly instead of creating an intersection
|
|
338
|
-
const resultWithRefinement = oneOfResult;
|
|
339
|
-
if (resultWithRefinement.isRefinementOnly && resultWithRefinement.refinementBody) {
|
|
340
|
-
output += `.superRefine(${resultWithRefinement.refinementBody})`;
|
|
341
|
-
// No intersection type needed - superRefine doesn't change the type
|
|
342
|
-
}
|
|
343
|
-
else {
|
|
344
|
-
output += `.and(${oneOfResult.expression})`;
|
|
345
|
-
intersectionTypes.push(oneOfResult.type);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
if (its.an.allOf(objectSchema)) {
|
|
349
|
-
const allOfResult = parseAllOf({
|
|
350
|
-
...objectSchema,
|
|
351
|
-
allOf: addObjectTypeAndMergeRequired(objectSchema.allOf),
|
|
352
|
-
}, refs);
|
|
353
|
-
output += `.and(${allOfResult.expression})`;
|
|
354
|
-
intersectionTypes.push(allOfResult.type);
|
|
355
|
-
}
|
|
356
|
-
// Handle if/then/else conditionals on object schemas
|
|
357
|
-
if (its.a.conditional(objectSchema)) {
|
|
358
|
-
const conditionalResult = parseIfThenElse(objectSchema, refs);
|
|
359
|
-
output += `.and(${conditionalResult.expression})`;
|
|
360
|
-
intersectionTypes.push(conditionalResult.type);
|
|
361
|
-
}
|
|
362
|
-
if (unevaluated === false && hasCompositionKeywords) {
|
|
363
|
-
const knownKeys = collectKnownPropertyKeys(objectSchema);
|
|
364
|
-
const patternRegexps = objectSchema.patternProperties
|
|
365
|
-
? Object.keys(objectSchema.patternProperties).map((pattern) => new RegExp(pattern))
|
|
366
|
-
: [];
|
|
367
|
-
const patternRegexpsLiteral = patternRegexps.length
|
|
368
|
-
? `[${patternRegexps.map((r) => r.toString()).join(", ")}]`
|
|
369
|
-
: "[new RegExp(\"$^\")]";
|
|
370
|
-
output += `.superRefine((value, ctx) => {
|
|
371
|
-
if (!value || typeof value !== "object") return;
|
|
372
|
-
const knownKeys = ${JSON.stringify(knownKeys)};
|
|
373
|
-
const patternRegexps = ${patternRegexpsLiteral};
|
|
374
|
-
for (const key in value) {
|
|
375
|
-
const isKnown = knownKeys.includes(key);
|
|
376
|
-
const matchesPattern = patternRegexps.length ? patternRegexps.some((r) => r.test(key)) : false;
|
|
377
|
-
if (!isKnown && !matchesPattern) {
|
|
378
|
-
ctx.addIssue({ code: "unrecognized_keys", keys: [key], path: [key], message: "Unknown property" });
|
|
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
|
+
});
|
|
379
123
|
}
|
|
380
|
-
|
|
381
|
-
|
|
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);
|
|
382
129
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
.join(" ");
|
|
389
|
-
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);
|
|
390
135
|
}
|
|
391
|
-
// propertyNames
|
|
136
|
+
// 5. propertyNames, unevaluatedProperties, dependentSchemas etc.
|
|
392
137
|
if (objectSchema.propertyNames) {
|
|
393
138
|
const normalizedPropNames = typeof objectSchema.propertyNames === "object" &&
|
|
394
139
|
objectSchema.propertyNames !== null &&
|
|
@@ -400,9 +145,9 @@ export function parseObject(objectSchema, refs) {
|
|
|
400
145
|
...refs,
|
|
401
146
|
path: [...refs.path, "propertyNames"],
|
|
402
147
|
});
|
|
403
|
-
|
|
148
|
+
finalExpr += `.superRefine((value, ctx) => {
|
|
404
149
|
for (const key in value) {
|
|
405
|
-
const result = ${propNameSchema}.safeParse(key);
|
|
150
|
+
const result = ${propNameSchema.expression}.safeParse(key);
|
|
406
151
|
if (!result.success) {
|
|
407
152
|
ctx.addIssue({
|
|
408
153
|
path: [key],
|
|
@@ -418,12 +163,15 @@ export function parseObject(objectSchema, refs) {
|
|
|
418
163
|
if (objectSchema.dependentSchemas && typeof objectSchema.dependentSchemas === "object") {
|
|
419
164
|
const entries = Object.entries(objectSchema.dependentSchemas);
|
|
420
165
|
if (entries.length) {
|
|
421
|
-
|
|
166
|
+
finalExpr += `.superRefine((obj, ctx) => {
|
|
422
167
|
${entries
|
|
423
168
|
.map(([key, schema]) => {
|
|
424
|
-
const parsed = parseSchema(schema, {
|
|
169
|
+
const parsed = parseSchema(schema, {
|
|
170
|
+
...refs,
|
|
171
|
+
path: [...refs.path, "dependentSchemas", key],
|
|
172
|
+
});
|
|
425
173
|
return `if (Object.prototype.hasOwnProperty.call(obj, ${JSON.stringify(key)})) {
|
|
426
|
-
const result = ${parsed}.safeParse(obj);
|
|
174
|
+
const result = ${parsed.expression}.safeParse(obj);
|
|
427
175
|
if (!result.success) {
|
|
428
176
|
ctx.addIssue({ code: "custom", message: ${objectSchema.errorMessage?.dependentSchemas ?? JSON.stringify("Dependent schema failed")}, path: [], params: { issues: result.error.issues } });
|
|
429
177
|
}
|
|
@@ -437,9 +185,9 @@ export function parseObject(objectSchema, refs) {
|
|
|
437
185
|
if (objectSchema.dependentRequired && typeof objectSchema.dependentRequired === "object") {
|
|
438
186
|
const entries = Object.entries(objectSchema.dependentRequired);
|
|
439
187
|
if (entries.length) {
|
|
440
|
-
const depRequiredMessage = objectSchema.errorMessage
|
|
441
|
-
"Dependent required properties missing";
|
|
442
|
-
|
|
188
|
+
const depRequiredMessage = objectSchema.errorMessage
|
|
189
|
+
?.dependentRequired ?? "Dependent required properties missing";
|
|
190
|
+
finalExpr += `.superRefine((obj, ctx) => {
|
|
443
191
|
${entries
|
|
444
192
|
.map(([prop, deps]) => {
|
|
445
193
|
const arr = Array.isArray(deps) ? deps : [];
|
|
@@ -458,73 +206,18 @@ export function parseObject(objectSchema, refs) {
|
|
|
458
206
|
})`;
|
|
459
207
|
}
|
|
460
208
|
}
|
|
461
|
-
//
|
|
462
|
-
let type;
|
|
463
|
-
if (propertyTypes.length
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
.map(({ key, type: propType }) => `${JSON.stringify(key)}: ${propType}`)
|
|
467
|
-
.join("; ");
|
|
468
|
-
type = `z.ZodObject<{ ${typeShape} }>`;
|
|
469
|
-
}
|
|
470
|
-
else if (properties === "z.object({})") {
|
|
471
|
-
// Empty object
|
|
472
|
-
type = "z.ZodObject<{}>";
|
|
473
|
-
}
|
|
474
|
-
else {
|
|
475
|
-
// Fallback for complex cases (patternProperties, record, etc.)
|
|
476
|
-
type = inferTypeFromExpression(output);
|
|
477
|
-
}
|
|
478
|
-
// Wrap in intersection types if .and() calls were added
|
|
479
|
-
for (const intersectionType of intersectionTypes) {
|
|
480
|
-
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}}>`;
|
|
481
214
|
}
|
|
215
|
+
// If intersections
|
|
216
|
+
intersectionTypes.forEach((t) => {
|
|
217
|
+
type = `z.ZodIntersection<${type}, ${t}>`;
|
|
218
|
+
});
|
|
482
219
|
return {
|
|
483
|
-
expression:
|
|
220
|
+
expression: finalExpr,
|
|
484
221
|
type,
|
|
485
222
|
};
|
|
486
223
|
}
|
|
487
|
-
/**
|
|
488
|
-
* Determines if a property should use getter syntax for recursive references.
|
|
489
|
-
* Getters defer evaluation until access time, which is the Zod v4 recommended
|
|
490
|
-
* approach for handling recursive schemas in object properties.
|
|
491
|
-
*/
|
|
492
|
-
const shouldUseGetter = (parsed, refs) => {
|
|
493
|
-
if (!parsed)
|
|
494
|
-
return false;
|
|
495
|
-
// Check for z.lazy() - these should use getters
|
|
496
|
-
if (parsed.includes("z.lazy("))
|
|
497
|
-
return true;
|
|
498
|
-
// Check for direct self-recursion (expression contains the current schema name)
|
|
499
|
-
// This handles cases like generateSchemaBundle where the schema name is different
|
|
500
|
-
// from the def name (e.g., NodeSchema vs node)
|
|
501
|
-
if (refs.currentSchemaName) {
|
|
502
|
-
const selfRefPattern = new RegExp(`\\b${refs.currentSchemaName}\\b`);
|
|
503
|
-
if (selfRefPattern.test(parsed)) {
|
|
504
|
-
return true;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
// Check for direct recursive references in the same SCC
|
|
508
|
-
if (refs.currentSchemaName && refs.cycleRefNames && refs.cycleComponentByName) {
|
|
509
|
-
const cycleRefNames = refs.cycleRefNames;
|
|
510
|
-
const cycleComponentByName = refs.cycleComponentByName;
|
|
511
|
-
const refNameArray = Array.from(cycleRefNames);
|
|
512
|
-
// Check if expression contains a reference to a cycle member in the same component
|
|
513
|
-
if (containsRecursiveRef(parsed, cycleRefNames)) {
|
|
514
|
-
const currentComponent = cycleComponentByName.get(refs.currentSchemaName);
|
|
515
|
-
if (currentComponent !== undefined) {
|
|
516
|
-
for (let i = 0; i < refNameArray.length; i++) {
|
|
517
|
-
const refName = refNameArray[i];
|
|
518
|
-
const pattern = new RegExp(`\\b${refName}\\b`);
|
|
519
|
-
if (pattern.test(parsed)) {
|
|
520
|
-
const refComponent = cycleComponentByName.get(refName);
|
|
521
|
-
if (refComponent === currentComponent) {
|
|
522
|
-
return true;
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
return false;
|
|
530
|
-
};
|