@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.
Files changed (53) hide show
  1. package/.github/RELEASE_SETUP.md +120 -0
  2. package/.github/TOOLING_GUIDE.md +169 -0
  3. package/.github/dependabot.yml +52 -0
  4. package/.github/workflows/ci.yml +33 -0
  5. package/.github/workflows/release.yml +12 -4
  6. package/.github/workflows/security.yml +40 -0
  7. package/.husky/commit-msg +1 -0
  8. package/.husky/pre-commit +1 -0
  9. package/.lintstagedrc.json +3 -0
  10. package/.prettierrc +20 -0
  11. package/AGENTS.md +7 -0
  12. package/CHANGELOG.md +13 -4
  13. package/README.md +9 -9
  14. package/commitlint.config.js +24 -0
  15. package/createIndex.ts +4 -4
  16. package/dist/cli.js +3 -4
  17. package/dist/core/analyzeSchema.js +28 -5
  18. package/dist/core/emitZod.js +11 -4
  19. package/dist/generators/generateBundle.js +67 -92
  20. package/dist/parsers/parseAllOf.js +11 -12
  21. package/dist/parsers/parseAnyOf.js +2 -2
  22. package/dist/parsers/parseArray.js +38 -12
  23. package/dist/parsers/parseMultipleType.js +2 -2
  24. package/dist/parsers/parseNumber.js +44 -102
  25. package/dist/parsers/parseObject.js +138 -393
  26. package/dist/parsers/parseOneOf.js +57 -100
  27. package/dist/parsers/parseSchema.js +132 -55
  28. package/dist/parsers/parseSimpleDiscriminatedOneOf.js +2 -2
  29. package/dist/parsers/parseString.js +113 -253
  30. package/dist/types/Types.d.ts +22 -1
  31. package/dist/types/core/analyzeSchema.d.ts +1 -0
  32. package/dist/types/generators/generateBundle.d.ts +1 -1
  33. package/dist/utils/cliTools.js +1 -2
  34. package/dist/utils/esmEmitter.js +6 -2
  35. package/dist/utils/extractInlineObject.js +1 -3
  36. package/dist/utils/jsdocs.js +1 -4
  37. package/dist/utils/liftInlineObjects.js +76 -15
  38. package/dist/utils/resolveRef.js +35 -10
  39. package/dist/utils/schemaRepresentation.js +35 -66
  40. package/dist/zodToJsonSchema.js +1 -2
  41. package/docs/IMPROVEMENT-PLAN.md +30 -12
  42. package/docs/ZOD-V4-RECURSIVE-TYPE-LIMITATIONS.md +70 -25
  43. package/docs/proposals/allof-required-merging.md +10 -4
  44. package/docs/proposals/bundle-refactor.md +10 -4
  45. package/docs/proposals/discriminated-union-with-default.md +18 -14
  46. package/docs/proposals/inline-object-lifting.md +15 -5
  47. package/docs/proposals/ref-anchor-support.md +11 -0
  48. package/output.txt +67 -0
  49. package/package.json +18 -5
  50. package/scripts/generateWorkflowSchema.ts +5 -14
  51. package/scripts/regenerate_bundle.ts +25 -0
  52. package/tsc_output.txt +542 -0
  53. 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 { its, parseSchema } from "./parseSchema.js";
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
- // 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
- }
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 (objectSchema.properties) {
71
- if (!Object.keys(objectSchema.properties).length) {
72
- properties = "z.object({})";
73
- }
74
- else {
75
- properties = "z.object({ ";
76
- properties += Object.keys(objectSchema.properties)
77
- .map((key) => {
78
- const propSchema = objectSchema.properties[key];
79
- const parsedProp = parseSchema(propSchema, {
80
- ...refs,
81
- path: [...refs.path, "properties", key],
82
- });
83
- const hasDefault = typeof propSchema === "object" && propSchema.default !== undefined;
84
- const required = Array.isArray(objectSchema.required)
85
- ? objectSchema.required.includes(key)
86
- : typeof propSchema === "object" && propSchema.required === true;
87
- const optional = !hasDefault && !required;
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
- else {
161
- patternProperties += `z.record(z.string(), ${patternExprs.join("")})`;
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
- for (const key in objectSchema.patternProperties) {
177
- patternProperties +=
178
- "if (key.match(new RegExp(" + JSON.stringify(key) + "))) {\n";
179
- if (additionalProperties) {
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 (additionalProperties) {
199
- patternProperties += "if (!evaluated) {\n";
200
- patternProperties +=
201
- "const result = " + additionalProperties.expression + ".safeParse(value[key])\n";
202
- patternProperties += "if (!result.success) {\n";
203
- patternProperties += `ctx.addIssue({
204
- path: [...(ctx.path ?? []), key],
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
- patternProperties += "}\n";
215
- patternProperties += "})";
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
- // Check if there will be an .and() call that adds properties from oneOf/anyOf/allOf/if-then-else
226
- // In that case, we should NOT use .strict() because it will reject the additional keys
227
- // before the union gets a chance to validate them.
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);
234
- let output = properties
235
- ? patternProperties
236
- ? properties + patternProperties
237
- : additionalProperties
238
- ? additionalProperties.expression === "z.never()"
239
- // Don't use .strict() if there are composition keywords that add properties
240
- ? hasCompositionKeywords
241
- ? properties
242
- : properties + ".strict()"
243
- : properties + `.catchall(${additionalProperties.expression})`
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
- const isKnown = ${JSON.stringify(knownKeys)}.includes(key);
270
- const matchesPattern = ${patterns.length ? "[" + patterns.map((r) => r.toString()).join(",") + "]" : "[]"}.some((r) => r.test(key));
271
- if (!isKnown && !matchesPattern) {
272
- const result = ${unevaluatedSchema.expression}.safeParse(value[key]);
273
- if (!result.success) {
274
- ctx.addIssue({ code: "custom", path: [key], message: "Invalid unevaluated property", params: { issues: result.error.issues } });
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
- // Track intersection types added via .and() calls
281
- const intersectionTypes = [];
282
- if (its.an.anyOf(objectSchema)) {
283
- const anyOfResult = parseAnyOf({
284
- ...objectSchema,
285
- anyOf: objectSchema.anyOf.map((x) => typeof x === "object" &&
286
- x !== null &&
287
- !x.type &&
288
- (x.properties || x.additionalProperties || x.patternProperties)
289
- ? { ...x, type: "object" }
290
- : x),
291
- }, refs);
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
- // Handle if/then/else conditionals on object schemas
326
- if (its.a.conditional(objectSchema)) {
327
- const conditionalResult = parseIfThenElse(objectSchema, refs);
328
- output += `.and(${conditionalResult.expression})`;
329
- intersectionTypes.push(conditionalResult.type);
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
- // 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} } })`;
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
- output += `.superRefine((value, ctx) => {
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
- output += `.superRefine((obj, ctx) => {
166
+ finalExpr += `.superRefine((obj, ctx) => {
370
167
  ${entries
371
168
  .map(([key, schema]) => {
372
- const parsed = parseSchema(schema, { ...refs, path: [...refs.path, "dependentSchemas", key] });
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?.dependentRequired ??
389
- "Dependent required properties missing";
390
- output += `.superRefine((obj, ctx) => {
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
- // 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}>`;
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: output,
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
- };