@gabrielbryk/json-schema-to-zod 2.10.1 → 2.11.1
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/AGENTS.md +44 -0
- package/CHANGELOG.md +38 -0
- package/README.md +6 -33
- package/check-types-lift.sh +23 -0
- package/check-types.sh +20 -0
- package/dist/{esm/cli.js → cli.js} +0 -6
- package/dist/{esm/core → core}/analyzeSchema.js +4 -5
- package/dist/core/emitZod.js +263 -0
- package/dist/{esm/generators → generators}/generateBundle.js +26 -13
- package/dist/{esm/index.js → index.js} +6 -0
- package/dist/jsonSchemaToZod.js +17 -0
- package/dist/parsers/parseAllOf.js +125 -0
- package/dist/parsers/parseAnyOf.js +28 -0
- package/dist/{esm/parsers → parsers}/parseArray.js +27 -11
- package/dist/parsers/parseBoolean.js +4 -0
- package/dist/parsers/parseConst.js +22 -0
- package/dist/parsers/parseEnum.js +35 -0
- package/dist/{esm/parsers → parsers}/parseIfThenElse.js +10 -6
- package/dist/parsers/parseMultipleType.js +10 -0
- package/dist/parsers/parseNot.js +14 -0
- package/dist/parsers/parseNull.js +4 -0
- package/dist/parsers/parseNullable.js +12 -0
- package/dist/{esm/parsers → parsers}/parseNumber.js +4 -1
- package/dist/{esm/parsers → parsers}/parseObject.js +200 -37
- package/dist/parsers/parseOneOf.js +365 -0
- package/dist/{esm/parsers → parsers}/parseSchema.js +55 -117
- package/dist/parsers/parseSimpleDiscriminatedOneOf.js +24 -0
- package/dist/{esm/parsers → parsers}/parseString.js +29 -18
- package/dist/types/Types.d.ts +32 -4
- package/dist/types/core/analyzeSchema.d.ts +3 -2
- package/dist/types/generators/generateBundle.d.ts +0 -2
- package/dist/types/index.d.ts +6 -0
- package/dist/types/parsers/parseAllOf.d.ts +2 -2
- package/dist/types/parsers/parseAnyOf.d.ts +2 -2
- package/dist/types/parsers/parseArray.d.ts +2 -2
- package/dist/types/parsers/parseBoolean.d.ts +2 -1
- package/dist/types/parsers/parseConst.d.ts +2 -2
- package/dist/types/parsers/parseDefault.d.ts +2 -2
- package/dist/types/parsers/parseEnum.d.ts +2 -2
- package/dist/types/parsers/parseIfThenElse.d.ts +2 -2
- package/dist/types/parsers/parseMultipleType.d.ts +2 -2
- package/dist/types/parsers/parseNot.d.ts +2 -2
- package/dist/types/parsers/parseNull.d.ts +2 -1
- package/dist/types/parsers/parseNullable.d.ts +2 -2
- package/dist/types/parsers/parseNumber.d.ts +2 -2
- package/dist/types/parsers/parseObject.d.ts +2 -2
- package/dist/types/parsers/parseOneOf.d.ts +2 -2
- package/dist/types/parsers/parseSchema.d.ts +2 -2
- package/dist/types/parsers/parseSimpleDiscriminatedOneOf.d.ts +2 -2
- package/dist/types/parsers/parseString.d.ts +2 -2
- package/dist/types/utils/anyOrUnknown.d.ts +5 -4
- package/dist/types/utils/esmEmitter.d.ts +29 -0
- package/dist/types/utils/extractInlineObject.d.ts +15 -0
- package/dist/types/utils/liftInlineObjects.d.ts +21 -0
- package/dist/types/utils/namingService.d.ts +21 -0
- package/dist/types/utils/resolveRef.d.ts +7 -0
- package/dist/types/utils/schemaRepresentation.d.ts +71 -0
- package/dist/utils/anyOrUnknown.js +13 -0
- package/dist/{esm/utils → utils}/buildRefRegistry.js +4 -0
- package/dist/utils/esmEmitter.js +87 -0
- package/dist/utils/extractInlineObject.js +119 -0
- package/dist/utils/liftInlineObjects.js +476 -0
- package/dist/utils/namingService.js +58 -0
- package/dist/utils/resolveRef.js +92 -0
- package/dist/utils/schemaRepresentation.js +569 -0
- package/docs/IMPROVEMENT-PLAN.md +243 -0
- package/docs/ZOD-V4-RECURSIVE-TYPE-LIMITATIONS.md +292 -0
- package/docs/proposals/bundle-refactor.md +1 -1
- package/docs/proposals/discriminated-union-with-default.md +248 -0
- package/docs/proposals/inline-object-lifting.md +77 -0
- package/eslint.config.js +4 -2
- package/jest.config.mjs +19 -0
- package/package.json +17 -20
- package/scripts/generateWorkflowSchema.ts +0 -1
- package/dist/cjs/Types.js +0 -2
- package/dist/cjs/cli.js +0 -70
- package/dist/cjs/core/analyzeSchema.js +0 -62
- package/dist/cjs/core/emitZod.js +0 -157
- package/dist/cjs/generators/generateBundle.js +0 -510
- package/dist/cjs/index.js +0 -50
- package/dist/cjs/jsonSchemaToZod.js +0 -10
- package/dist/cjs/package.json +0 -1
- package/dist/cjs/parsers/parseAllOf.js +0 -46
- package/dist/cjs/parsers/parseAnyOf.js +0 -18
- package/dist/cjs/parsers/parseArray.js +0 -90
- package/dist/cjs/parsers/parseBoolean.js +0 -5
- package/dist/cjs/parsers/parseConst.js +0 -7
- package/dist/cjs/parsers/parseDefault.js +0 -8
- package/dist/cjs/parsers/parseEnum.js +0 -21
- package/dist/cjs/parsers/parseIfThenElse.js +0 -35
- package/dist/cjs/parsers/parseMultipleType.js +0 -10
- package/dist/cjs/parsers/parseNot.js +0 -12
- package/dist/cjs/parsers/parseNull.js +0 -5
- package/dist/cjs/parsers/parseNullable.js +0 -12
- package/dist/cjs/parsers/parseNumber.js +0 -116
- package/dist/cjs/parsers/parseObject.js +0 -318
- package/dist/cjs/parsers/parseOneOf.js +0 -53
- package/dist/cjs/parsers/parseSchema.js +0 -419
- package/dist/cjs/parsers/parseSimpleDiscriminatedOneOf.js +0 -21
- package/dist/cjs/parsers/parseString.js +0 -317
- package/dist/cjs/utils/anyOrUnknown.js +0 -14
- package/dist/cjs/utils/buildRefRegistry.js +0 -56
- package/dist/cjs/utils/cliTools.js +0 -108
- package/dist/cjs/utils/cycles.js +0 -113
- package/dist/cjs/utils/half.js +0 -7
- package/dist/cjs/utils/jsdocs.js +0 -20
- package/dist/cjs/utils/omit.js +0 -11
- package/dist/cjs/utils/resolveUri.js +0 -16
- package/dist/cjs/utils/withMessage.js +0 -21
- package/dist/cjs/zodToJsonSchema.js +0 -89
- package/dist/esm/core/emitZod.js +0 -153
- package/dist/esm/jsonSchemaToZod.js +0 -6
- package/dist/esm/package.json +0 -1
- package/dist/esm/parsers/parseAllOf.js +0 -43
- package/dist/esm/parsers/parseAnyOf.js +0 -14
- package/dist/esm/parsers/parseBoolean.js +0 -1
- package/dist/esm/parsers/parseConst.js +0 -3
- package/dist/esm/parsers/parseEnum.js +0 -17
- package/dist/esm/parsers/parseMultipleType.js +0 -6
- package/dist/esm/parsers/parseNot.js +0 -8
- package/dist/esm/parsers/parseNull.js +0 -1
- package/dist/esm/parsers/parseNullable.js +0 -8
- package/dist/esm/parsers/parseOneOf.js +0 -49
- package/dist/esm/parsers/parseSimpleDiscriminatedOneOf.js +0 -17
- package/dist/esm/utils/anyOrUnknown.js +0 -10
- package/jest.config.cjs +0 -4
- package/postcjs.cjs +0 -1
- package/postesm.cjs +0 -1
- /package/dist/{esm/Types.js → Types.js} +0 -0
- /package/dist/{esm/parsers → parsers}/parseDefault.js +0 -0
- /package/dist/{esm/utils → utils}/cliTools.js +0 -0
- /package/dist/{esm/utils → utils}/cycles.js +0 -0
- /package/dist/{esm/utils → utils}/half.js +0 -0
- /package/dist/{esm/utils → utils}/jsdocs.js +0 -0
- /package/dist/{esm/utils → utils}/omit.js +0 -0
- /package/dist/{esm/utils → utils}/resolveUri.js +0 -0
- /package/dist/{esm/utils → utils}/withMessage.js +0 -0
- /package/dist/{esm/zodToJsonSchema.js → zodToJsonSchema.js} +0 -0
|
@@ -5,8 +5,68 @@ import { parseAllOf } from "./parseAllOf.js";
|
|
|
5
5
|
import { parseIfThenElse } from "./parseIfThenElse.js";
|
|
6
6
|
import { addJsdocs } from "../utils/jsdocs.js";
|
|
7
7
|
import { anyOrUnknown } from "../utils/anyOrUnknown.js";
|
|
8
|
+
import { containsRecursiveRef, inferTypeFromExpression } from "../utils/schemaRepresentation.js";
|
|
8
9
|
export function parseObject(objectSchema, refs) {
|
|
10
|
+
// Optimization: if we have composition keywords (allOf/anyOf/oneOf) but no direct properties,
|
|
11
|
+
// delegate entirely to the composition parser to avoid generating z.object({}).and(...)
|
|
12
|
+
const hasDirectProperties = objectSchema.properties && Object.keys(objectSchema.properties).length > 0;
|
|
13
|
+
const hasAdditionalProperties = objectSchema.additionalProperties !== undefined;
|
|
14
|
+
const hasPatternProperties = objectSchema.patternProperties !== undefined;
|
|
15
|
+
const hasNoDirectSchema = !hasDirectProperties && !hasAdditionalProperties && !hasPatternProperties;
|
|
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
|
+
}
|
|
9
67
|
let properties = undefined;
|
|
68
|
+
// Track property types for building proper object type annotations
|
|
69
|
+
const propertyTypes = [];
|
|
10
70
|
if (objectSchema.properties) {
|
|
11
71
|
if (!Object.keys(objectSchema.properties).length) {
|
|
12
72
|
properties = "z.object({})";
|
|
@@ -26,10 +86,18 @@ export function parseObject(objectSchema, refs) {
|
|
|
26
86
|
: typeof propSchema === "object" && propSchema.required === true;
|
|
27
87
|
const optional = !hasDefault && !required;
|
|
28
88
|
const valueWithOptional = optional
|
|
29
|
-
? `${parsedProp}.optional()`
|
|
30
|
-
: parsedProp;
|
|
31
|
-
|
|
32
|
-
|
|
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} }`
|
|
33
101
|
: `${JSON.stringify(key)}: ${valueWithOptional}`;
|
|
34
102
|
if (refs.withJsdocs && typeof propSchema === "object") {
|
|
35
103
|
result = addJsdocs(propSchema, result);
|
|
@@ -47,6 +115,10 @@ export function parseObject(objectSchema, refs) {
|
|
|
47
115
|
})
|
|
48
116
|
: undefined;
|
|
49
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
|
+
: [];
|
|
50
122
|
let patternProperties = undefined;
|
|
51
123
|
if (objectSchema.patternProperties) {
|
|
52
124
|
const parsedPatternProperties = Object.fromEntries(Object.entries(objectSchema.patternProperties).map(([key, value]) => {
|
|
@@ -58,33 +130,35 @@ export function parseObject(objectSchema, refs) {
|
|
|
58
130
|
}),
|
|
59
131
|
];
|
|
60
132
|
}, {}));
|
|
133
|
+
// Helper to get expressions from parsed pattern properties
|
|
134
|
+
const patternExprs = Object.values(parsedPatternProperties).map(r => r.expression);
|
|
61
135
|
patternProperties = "";
|
|
62
136
|
if (properties) {
|
|
63
137
|
if (additionalProperties) {
|
|
64
138
|
patternProperties += `.catchall(z.union([${[
|
|
65
|
-
...
|
|
66
|
-
additionalProperties,
|
|
139
|
+
...patternExprs,
|
|
140
|
+
additionalProperties.expression,
|
|
67
141
|
].join(", ")}]))`;
|
|
68
142
|
}
|
|
69
143
|
else if (Object.keys(parsedPatternProperties).length > 1) {
|
|
70
|
-
patternProperties += `.catchall(z.union([${
|
|
144
|
+
patternProperties += `.catchall(z.union([${patternExprs.join(", ")}]))`;
|
|
71
145
|
}
|
|
72
146
|
else {
|
|
73
|
-
patternProperties += `.catchall(${
|
|
147
|
+
patternProperties += `.catchall(${patternExprs.join("")})`;
|
|
74
148
|
}
|
|
75
149
|
}
|
|
76
150
|
else {
|
|
77
151
|
if (additionalProperties) {
|
|
78
152
|
patternProperties += `z.record(z.string(), z.union([${[
|
|
79
|
-
...
|
|
80
|
-
additionalProperties,
|
|
153
|
+
...patternExprs,
|
|
154
|
+
additionalProperties.expression,
|
|
81
155
|
].join(", ")}]))`;
|
|
82
156
|
}
|
|
83
157
|
else if (Object.keys(parsedPatternProperties).length > 1) {
|
|
84
|
-
patternProperties += `z.record(z.string(), z.union([${
|
|
158
|
+
patternProperties += `z.record(z.string(), z.union([${patternExprs.join(", ")}]))`;
|
|
85
159
|
}
|
|
86
160
|
else {
|
|
87
|
-
patternProperties += `z.record(z.string(), ${
|
|
161
|
+
patternProperties += `z.record(z.string(), ${patternExprs.join("")})`;
|
|
88
162
|
}
|
|
89
163
|
}
|
|
90
164
|
patternProperties += ".superRefine((value, ctx) => {\n";
|
|
@@ -107,7 +181,7 @@ export function parseObject(objectSchema, refs) {
|
|
|
107
181
|
}
|
|
108
182
|
patternProperties +=
|
|
109
183
|
"const result = " +
|
|
110
|
-
parsedPatternProperties[key] +
|
|
184
|
+
parsedPatternProperties[key].expression +
|
|
111
185
|
".safeParse(value[key])\n";
|
|
112
186
|
patternProperties += "if (!result.success) {\n";
|
|
113
187
|
patternProperties += `ctx.addIssue({
|
|
@@ -124,7 +198,7 @@ export function parseObject(objectSchema, refs) {
|
|
|
124
198
|
if (additionalProperties) {
|
|
125
199
|
patternProperties += "if (!evaluated) {\n";
|
|
126
200
|
patternProperties +=
|
|
127
|
-
"const result = " + additionalProperties + ".safeParse(value[key])\n";
|
|
201
|
+
"const result = " + additionalProperties.expression + ".safeParse(value[key])\n";
|
|
128
202
|
patternProperties += "if (!result.success) {\n";
|
|
129
203
|
patternProperties += `ctx.addIssue({
|
|
130
204
|
path: [...(ctx.path ?? []), key],
|
|
@@ -152,22 +226,32 @@ export function parseObject(objectSchema, refs) {
|
|
|
152
226
|
// In that case, we should NOT use .strict() because it will reject the additional keys
|
|
153
227
|
// before the union gets a chance to validate them.
|
|
154
228
|
const hasCompositionKeywords = its.an.anyOf(objectSchema) || its.a.oneOf(objectSchema) || its.an.allOf(objectSchema) || its.a.conditional(objectSchema);
|
|
229
|
+
// When there are composition keywords (allOf, anyOf, oneOf, if-then-else) but no direct properties,
|
|
230
|
+
// we should NOT default to z.record(z.string(), z.any()) because that would allow any properties.
|
|
231
|
+
// Instead, use z.object({}) and let the .and() call add properties from the composition.
|
|
232
|
+
// This is especially important when unevaluatedProperties: false is set.
|
|
233
|
+
const fallback = anyOrUnknown(refs);
|
|
155
234
|
let output = properties
|
|
156
235
|
? patternProperties
|
|
157
236
|
? properties + patternProperties
|
|
158
237
|
: additionalProperties
|
|
159
|
-
? additionalProperties === "z.never()"
|
|
238
|
+
? additionalProperties.expression === "z.never()"
|
|
160
239
|
// Don't use .strict() if there are composition keywords that add properties
|
|
161
240
|
? hasCompositionKeywords
|
|
162
241
|
? properties
|
|
163
242
|
: properties + ".strict()"
|
|
164
|
-
: properties + `.catchall(${additionalProperties})`
|
|
243
|
+
: properties + `.catchall(${additionalProperties.expression})`
|
|
165
244
|
: properties
|
|
166
245
|
: patternProperties
|
|
167
246
|
? patternProperties
|
|
168
247
|
: additionalProperties
|
|
169
|
-
? `z.record(z.string(), ${additionalProperties})`
|
|
170
|
-
|
|
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})`;
|
|
171
255
|
if (unevaluated === false && properties && !hasCompositionKeywords) {
|
|
172
256
|
output += ".strict()";
|
|
173
257
|
}
|
|
@@ -185,7 +269,7 @@ export function parseObject(objectSchema, refs) {
|
|
|
185
269
|
const isKnown = ${JSON.stringify(knownKeys)}.includes(key);
|
|
186
270
|
const matchesPattern = ${patterns.length ? "[" + patterns.map((r) => r.toString()).join(",") + "]" : "[]"}.some((r) => r.test(key));
|
|
187
271
|
if (!isKnown && !matchesPattern) {
|
|
188
|
-
const result = ${unevaluatedSchema}.safeParse(value[key]);
|
|
272
|
+
const result = ${unevaluatedSchema.expression}.safeParse(value[key]);
|
|
189
273
|
if (!result.success) {
|
|
190
274
|
ctx.addIssue({ code: "custom", path: [key], message: "Invalid unevaluated property", params: { issues: result.error.issues } });
|
|
191
275
|
}
|
|
@@ -193,8 +277,10 @@ export function parseObject(objectSchema, refs) {
|
|
|
193
277
|
}
|
|
194
278
|
})`;
|
|
195
279
|
}
|
|
280
|
+
// Track intersection types added via .and() calls
|
|
281
|
+
const intersectionTypes = [];
|
|
196
282
|
if (its.an.anyOf(objectSchema)) {
|
|
197
|
-
|
|
283
|
+
const anyOfResult = parseAnyOf({
|
|
198
284
|
...objectSchema,
|
|
199
285
|
anyOf: objectSchema.anyOf.map((x) => typeof x === "object" &&
|
|
200
286
|
x !== null &&
|
|
@@ -202,10 +288,12 @@ export function parseObject(objectSchema, refs) {
|
|
|
202
288
|
(x.properties || x.additionalProperties || x.patternProperties)
|
|
203
289
|
? { ...x, type: "object" }
|
|
204
290
|
: x),
|
|
205
|
-
}, refs)
|
|
291
|
+
}, refs);
|
|
292
|
+
output += `.and(${anyOfResult.expression})`;
|
|
293
|
+
intersectionTypes.push(anyOfResult.type);
|
|
206
294
|
}
|
|
207
295
|
if (its.a.oneOf(objectSchema)) {
|
|
208
|
-
|
|
296
|
+
const oneOfResult = parseOneOf({
|
|
209
297
|
...objectSchema,
|
|
210
298
|
oneOf: objectSchema.oneOf.map((x) => typeof x === "object" &&
|
|
211
299
|
x !== null &&
|
|
@@ -213,22 +301,40 @@ export function parseObject(objectSchema, refs) {
|
|
|
213
301
|
(x.properties || x.additionalProperties || x.patternProperties)
|
|
214
302
|
? { ...x, type: "object" }
|
|
215
303
|
: x),
|
|
216
|
-
}, refs)
|
|
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
|
+
}
|
|
217
316
|
}
|
|
218
317
|
if (its.an.allOf(objectSchema)) {
|
|
219
|
-
|
|
318
|
+
const allOfResult = parseAllOf({
|
|
220
319
|
...objectSchema,
|
|
221
|
-
allOf: objectSchema.allOf
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
? { ...x, type: "object" }
|
|
226
|
-
: x),
|
|
227
|
-
}, refs)})`;
|
|
320
|
+
allOf: addObjectTypeAndMergeRequired(objectSchema.allOf),
|
|
321
|
+
}, refs);
|
|
322
|
+
output += `.and(${allOfResult.expression})`;
|
|
323
|
+
intersectionTypes.push(allOfResult.type);
|
|
228
324
|
}
|
|
229
325
|
// Handle if/then/else conditionals on object schemas
|
|
230
326
|
if (its.a.conditional(objectSchema)) {
|
|
231
|
-
|
|
327
|
+
const conditionalResult = parseIfThenElse(objectSchema, refs);
|
|
328
|
+
output += `.and(${conditionalResult.expression})`;
|
|
329
|
+
intersectionTypes.push(conditionalResult.type);
|
|
330
|
+
}
|
|
331
|
+
// Only add required validation for missing keys when there are no composition keywords
|
|
332
|
+
// When allOf/anyOf/oneOf exist, they should define the properties and handle required validation
|
|
333
|
+
if (missingRequiredKeys.length > 0 && !hasCompositionKeywords) {
|
|
334
|
+
const checks = missingRequiredKeys
|
|
335
|
+
.map((key) => `if (!Object.prototype.hasOwnProperty.call(value, ${JSON.stringify(key)})) { ctx.addIssue({ code: "custom", path: [${JSON.stringify(key)}], message: "Required property missing" }); }`)
|
|
336
|
+
.join(" ");
|
|
337
|
+
output += `.superRefine((value, ctx) => { if (value && typeof value === "object") { ${checks} } })`;
|
|
232
338
|
}
|
|
233
339
|
// propertyNames
|
|
234
340
|
if (objectSchema.propertyNames) {
|
|
@@ -300,16 +406,73 @@ export function parseObject(objectSchema, refs) {
|
|
|
300
406
|
})`;
|
|
301
407
|
}
|
|
302
408
|
}
|
|
303
|
-
|
|
409
|
+
// Build the type representation from tracked property types
|
|
410
|
+
let type;
|
|
411
|
+
if (propertyTypes.length > 0) {
|
|
412
|
+
// Build proper object type with actual property types
|
|
413
|
+
const typeShape = propertyTypes
|
|
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}>`;
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
expression: output,
|
|
432
|
+
type,
|
|
433
|
+
};
|
|
304
434
|
}
|
|
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
|
+
*/
|
|
305
440
|
const shouldUseGetter = (parsed, refs) => {
|
|
306
441
|
if (!parsed)
|
|
307
442
|
return false;
|
|
308
|
-
|
|
443
|
+
// Check for z.lazy() - these should use getters
|
|
444
|
+
if (parsed.includes("z.lazy("))
|
|
309
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
|
+
}
|
|
310
454
|
}
|
|
311
|
-
|
|
312
|
-
|
|
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
|
+
}
|
|
313
476
|
}
|
|
314
|
-
return
|
|
477
|
+
return false;
|
|
315
478
|
};
|