@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.
Files changed (138) hide show
  1. package/AGENTS.md +44 -0
  2. package/CHANGELOG.md +38 -0
  3. package/README.md +6 -33
  4. package/check-types-lift.sh +23 -0
  5. package/check-types.sh +20 -0
  6. package/dist/{esm/cli.js → cli.js} +0 -6
  7. package/dist/{esm/core → core}/analyzeSchema.js +4 -5
  8. package/dist/core/emitZod.js +263 -0
  9. package/dist/{esm/generators → generators}/generateBundle.js +26 -13
  10. package/dist/{esm/index.js → index.js} +6 -0
  11. package/dist/jsonSchemaToZod.js +17 -0
  12. package/dist/parsers/parseAllOf.js +125 -0
  13. package/dist/parsers/parseAnyOf.js +28 -0
  14. package/dist/{esm/parsers → parsers}/parseArray.js +27 -11
  15. package/dist/parsers/parseBoolean.js +4 -0
  16. package/dist/parsers/parseConst.js +22 -0
  17. package/dist/parsers/parseEnum.js +35 -0
  18. package/dist/{esm/parsers → parsers}/parseIfThenElse.js +10 -6
  19. package/dist/parsers/parseMultipleType.js +10 -0
  20. package/dist/parsers/parseNot.js +14 -0
  21. package/dist/parsers/parseNull.js +4 -0
  22. package/dist/parsers/parseNullable.js +12 -0
  23. package/dist/{esm/parsers → parsers}/parseNumber.js +4 -1
  24. package/dist/{esm/parsers → parsers}/parseObject.js +200 -37
  25. package/dist/parsers/parseOneOf.js +365 -0
  26. package/dist/{esm/parsers → parsers}/parseSchema.js +55 -117
  27. package/dist/parsers/parseSimpleDiscriminatedOneOf.js +24 -0
  28. package/dist/{esm/parsers → parsers}/parseString.js +29 -18
  29. package/dist/types/Types.d.ts +32 -4
  30. package/dist/types/core/analyzeSchema.d.ts +3 -2
  31. package/dist/types/generators/generateBundle.d.ts +0 -2
  32. package/dist/types/index.d.ts +6 -0
  33. package/dist/types/parsers/parseAllOf.d.ts +2 -2
  34. package/dist/types/parsers/parseAnyOf.d.ts +2 -2
  35. package/dist/types/parsers/parseArray.d.ts +2 -2
  36. package/dist/types/parsers/parseBoolean.d.ts +2 -1
  37. package/dist/types/parsers/parseConst.d.ts +2 -2
  38. package/dist/types/parsers/parseDefault.d.ts +2 -2
  39. package/dist/types/parsers/parseEnum.d.ts +2 -2
  40. package/dist/types/parsers/parseIfThenElse.d.ts +2 -2
  41. package/dist/types/parsers/parseMultipleType.d.ts +2 -2
  42. package/dist/types/parsers/parseNot.d.ts +2 -2
  43. package/dist/types/parsers/parseNull.d.ts +2 -1
  44. package/dist/types/parsers/parseNullable.d.ts +2 -2
  45. package/dist/types/parsers/parseNumber.d.ts +2 -2
  46. package/dist/types/parsers/parseObject.d.ts +2 -2
  47. package/dist/types/parsers/parseOneOf.d.ts +2 -2
  48. package/dist/types/parsers/parseSchema.d.ts +2 -2
  49. package/dist/types/parsers/parseSimpleDiscriminatedOneOf.d.ts +2 -2
  50. package/dist/types/parsers/parseString.d.ts +2 -2
  51. package/dist/types/utils/anyOrUnknown.d.ts +5 -4
  52. package/dist/types/utils/esmEmitter.d.ts +29 -0
  53. package/dist/types/utils/extractInlineObject.d.ts +15 -0
  54. package/dist/types/utils/liftInlineObjects.d.ts +21 -0
  55. package/dist/types/utils/namingService.d.ts +21 -0
  56. package/dist/types/utils/resolveRef.d.ts +7 -0
  57. package/dist/types/utils/schemaRepresentation.d.ts +71 -0
  58. package/dist/utils/anyOrUnknown.js +13 -0
  59. package/dist/{esm/utils → utils}/buildRefRegistry.js +4 -0
  60. package/dist/utils/esmEmitter.js +87 -0
  61. package/dist/utils/extractInlineObject.js +119 -0
  62. package/dist/utils/liftInlineObjects.js +476 -0
  63. package/dist/utils/namingService.js +58 -0
  64. package/dist/utils/resolveRef.js +92 -0
  65. package/dist/utils/schemaRepresentation.js +569 -0
  66. package/docs/IMPROVEMENT-PLAN.md +243 -0
  67. package/docs/ZOD-V4-RECURSIVE-TYPE-LIMITATIONS.md +292 -0
  68. package/docs/proposals/bundle-refactor.md +1 -1
  69. package/docs/proposals/discriminated-union-with-default.md +248 -0
  70. package/docs/proposals/inline-object-lifting.md +77 -0
  71. package/eslint.config.js +4 -2
  72. package/jest.config.mjs +19 -0
  73. package/package.json +17 -20
  74. package/scripts/generateWorkflowSchema.ts +0 -1
  75. package/dist/cjs/Types.js +0 -2
  76. package/dist/cjs/cli.js +0 -70
  77. package/dist/cjs/core/analyzeSchema.js +0 -62
  78. package/dist/cjs/core/emitZod.js +0 -157
  79. package/dist/cjs/generators/generateBundle.js +0 -510
  80. package/dist/cjs/index.js +0 -50
  81. package/dist/cjs/jsonSchemaToZod.js +0 -10
  82. package/dist/cjs/package.json +0 -1
  83. package/dist/cjs/parsers/parseAllOf.js +0 -46
  84. package/dist/cjs/parsers/parseAnyOf.js +0 -18
  85. package/dist/cjs/parsers/parseArray.js +0 -90
  86. package/dist/cjs/parsers/parseBoolean.js +0 -5
  87. package/dist/cjs/parsers/parseConst.js +0 -7
  88. package/dist/cjs/parsers/parseDefault.js +0 -8
  89. package/dist/cjs/parsers/parseEnum.js +0 -21
  90. package/dist/cjs/parsers/parseIfThenElse.js +0 -35
  91. package/dist/cjs/parsers/parseMultipleType.js +0 -10
  92. package/dist/cjs/parsers/parseNot.js +0 -12
  93. package/dist/cjs/parsers/parseNull.js +0 -5
  94. package/dist/cjs/parsers/parseNullable.js +0 -12
  95. package/dist/cjs/parsers/parseNumber.js +0 -116
  96. package/dist/cjs/parsers/parseObject.js +0 -318
  97. package/dist/cjs/parsers/parseOneOf.js +0 -53
  98. package/dist/cjs/parsers/parseSchema.js +0 -419
  99. package/dist/cjs/parsers/parseSimpleDiscriminatedOneOf.js +0 -21
  100. package/dist/cjs/parsers/parseString.js +0 -317
  101. package/dist/cjs/utils/anyOrUnknown.js +0 -14
  102. package/dist/cjs/utils/buildRefRegistry.js +0 -56
  103. package/dist/cjs/utils/cliTools.js +0 -108
  104. package/dist/cjs/utils/cycles.js +0 -113
  105. package/dist/cjs/utils/half.js +0 -7
  106. package/dist/cjs/utils/jsdocs.js +0 -20
  107. package/dist/cjs/utils/omit.js +0 -11
  108. package/dist/cjs/utils/resolveUri.js +0 -16
  109. package/dist/cjs/utils/withMessage.js +0 -21
  110. package/dist/cjs/zodToJsonSchema.js +0 -89
  111. package/dist/esm/core/emitZod.js +0 -153
  112. package/dist/esm/jsonSchemaToZod.js +0 -6
  113. package/dist/esm/package.json +0 -1
  114. package/dist/esm/parsers/parseAllOf.js +0 -43
  115. package/dist/esm/parsers/parseAnyOf.js +0 -14
  116. package/dist/esm/parsers/parseBoolean.js +0 -1
  117. package/dist/esm/parsers/parseConst.js +0 -3
  118. package/dist/esm/parsers/parseEnum.js +0 -17
  119. package/dist/esm/parsers/parseMultipleType.js +0 -6
  120. package/dist/esm/parsers/parseNot.js +0 -8
  121. package/dist/esm/parsers/parseNull.js +0 -1
  122. package/dist/esm/parsers/parseNullable.js +0 -8
  123. package/dist/esm/parsers/parseOneOf.js +0 -49
  124. package/dist/esm/parsers/parseSimpleDiscriminatedOneOf.js +0 -17
  125. package/dist/esm/utils/anyOrUnknown.js +0 -10
  126. package/jest.config.cjs +0 -4
  127. package/postcjs.cjs +0 -1
  128. package/postesm.cjs +0 -1
  129. /package/dist/{esm/Types.js → Types.js} +0 -0
  130. /package/dist/{esm/parsers → parsers}/parseDefault.js +0 -0
  131. /package/dist/{esm/utils → utils}/cliTools.js +0 -0
  132. /package/dist/{esm/utils → utils}/cycles.js +0 -0
  133. /package/dist/{esm/utils → utils}/half.js +0 -0
  134. /package/dist/{esm/utils → utils}/jsdocs.js +0 -0
  135. /package/dist/{esm/utils → utils}/omit.js +0 -0
  136. /package/dist/{esm/utils → utils}/resolveUri.js +0 -0
  137. /package/dist/{esm/utils → utils}/withMessage.js +0 -0
  138. /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
- let result = shouldUseGetter(valueWithOptional, refs)
32
- ? `get ${JSON.stringify(key)}(){ return ${valueWithOptional} }`
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
- ...Object.values(parsedPatternProperties),
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([${Object.values(parsedPatternProperties).join(", ")}]))`;
144
+ patternProperties += `.catchall(z.union([${patternExprs.join(", ")}]))`;
71
145
  }
72
146
  else {
73
- patternProperties += `.catchall(${Object.values(parsedPatternProperties)})`;
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
- ...Object.values(parsedPatternProperties),
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([${Object.values(parsedPatternProperties).join(", ")}]))`;
158
+ patternProperties += `z.record(z.string(), z.union([${patternExprs.join(", ")}]))`;
85
159
  }
86
160
  else {
87
- patternProperties += `z.record(z.string(), ${Object.values(parsedPatternProperties)})`;
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
- : `z.record(z.string(), ${anyOrUnknown(refs)})`;
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
- output += `.and(${parseAnyOf({
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
- output += `.and(${parseOneOf({
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
- output += `.and(${parseAllOf({
318
+ const allOfResult = parseAllOf({
220
319
  ...objectSchema,
221
- allOf: objectSchema.allOf.map((x) => typeof x === "object" &&
222
- x !== null &&
223
- !x.type &&
224
- (x.properties || x.additionalProperties || x.patternProperties)
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
- output += `.and(${parseIfThenElse(objectSchema, refs)})`;
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
- return output;
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
- if (refs.currentSchemaName && parsed.includes(refs.currentSchemaName)) {
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
- if (refs.cycleRefNames?.has(parsed)) {
312
- return true;
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 Boolean(refs.inProgress && refs.inProgress.has(parsed));
477
+ return false;
315
478
  };