@gabrielbryk/json-schema-to-zod 2.12.1 → 2.14.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 (57) 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 +24 -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 +56 -11
  18. package/dist/core/emitZod.js +43 -12
  19. package/dist/generators/generateBundle.js +67 -92
  20. package/dist/index.js +1 -0
  21. package/dist/parsers/parseAllOf.js +11 -12
  22. package/dist/parsers/parseAnyOf.js +2 -2
  23. package/dist/parsers/parseArray.js +38 -12
  24. package/dist/parsers/parseMultipleType.js +2 -2
  25. package/dist/parsers/parseNumber.js +44 -102
  26. package/dist/parsers/parseObject.js +136 -443
  27. package/dist/parsers/parseOneOf.js +57 -110
  28. package/dist/parsers/parseSchema.js +176 -71
  29. package/dist/parsers/parseSimpleDiscriminatedOneOf.js +2 -2
  30. package/dist/parsers/parseString.js +113 -253
  31. package/dist/types/Types.d.ts +37 -1
  32. package/dist/types/core/analyzeSchema.d.ts +4 -0
  33. package/dist/types/generators/generateBundle.d.ts +1 -1
  34. package/dist/types/index.d.ts +1 -0
  35. package/dist/types/utils/schemaNaming.d.ts +6 -0
  36. package/dist/utils/cliTools.js +1 -2
  37. package/dist/utils/esmEmitter.js +6 -2
  38. package/dist/utils/extractInlineObject.js +1 -3
  39. package/dist/utils/jsdocs.js +1 -4
  40. package/dist/utils/liftInlineObjects.js +76 -15
  41. package/dist/utils/resolveRef.js +35 -10
  42. package/dist/utils/schemaNaming.js +31 -0
  43. package/dist/utils/schemaRepresentation.js +35 -66
  44. package/dist/zodToJsonSchema.js +1 -2
  45. package/docs/IMPROVEMENT-PLAN.md +30 -12
  46. package/docs/ZOD-V4-RECURSIVE-TYPE-LIMITATIONS.md +70 -25
  47. package/docs/proposals/allof-required-merging.md +10 -4
  48. package/docs/proposals/bundle-refactor.md +10 -4
  49. package/docs/proposals/discriminated-union-with-default.md +18 -14
  50. package/docs/proposals/inline-object-lifting.md +15 -5
  51. package/docs/proposals/ref-anchor-support.md +11 -0
  52. package/output.txt +67 -0
  53. package/package.json +18 -5
  54. package/scripts/generateWorkflowSchema.ts +5 -14
  55. package/scripts/regenerate_bundle.ts +25 -0
  56. package/tsc_output.txt +542 -0
  57. 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 { 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
- 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
- // Optimization: if we have composition keywords (allOf/anyOf/oneOf) but no direct properties,
30
- // delegate entirely to the composition parser to avoid generating z.object({}).and(...)
31
- const hasDirectProperties = objectSchema.properties && Object.keys(objectSchema.properties).length > 0;
32
- const hasAdditionalProperties = objectSchema.additionalProperties !== undefined;
33
- const hasPatternProperties = objectSchema.patternProperties !== undefined;
34
- const hasNoDirectSchema = !hasDirectProperties && !hasAdditionalProperties && !hasPatternProperties;
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 (objectSchema.properties) {
90
- if (!Object.keys(objectSchema.properties).length) {
91
- properties = "z.object({})";
92
- }
93
- else {
94
- properties = "z.object({ ";
95
- properties += Object.keys(objectSchema.properties)
96
- .map((key) => {
97
- const propSchema = objectSchema.properties[key];
98
- const parsedProp = parseSchema(propSchema, {
99
- ...refs,
100
- path: [...refs.path, "properties", key],
101
- });
102
- const hasDefault = typeof propSchema === "object" && propSchema.default !== undefined;
103
- const required = Array.isArray(objectSchema.required)
104
- ? objectSchema.required.includes(key)
105
- : typeof propSchema === "object" && propSchema.required === true;
106
- const optional = !hasDefault && !required;
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
- patternProperties += ".superRefine((value, ctx) => {\n";
184
- patternProperties += "for (const key in value) {\n";
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
- for (const key in objectSchema.patternProperties) {
196
- patternProperties +=
197
- "if (key.match(new RegExp(" + JSON.stringify(key) + "))) {\n";
198
- if (additionalProperties) {
199
- patternProperties += "evaluated = true\n";
200
- }
201
- patternProperties +=
202
- "const result = " +
203
- parsedPatternProperties[key].expression +
204
- ".safeParse(value[key])\n";
205
- patternProperties += "if (!result.success) {\n";
206
- patternProperties += `ctx.addIssue({
207
- path: [...(ctx.path ?? []), key],
208
- code: 'custom',
209
- message: \`Invalid input: Key matching regex /\${key}/ must match schema\`,
210
- params: {
211
- issues: result.error.issues
212
- }
213
- })\n`;
214
- patternProperties += "}\n";
215
- patternProperties += "}\n";
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
- // Check if there will be an .and() call that adds properties from oneOf/anyOf/allOf/if-then-else
245
- // In that case, we should NOT use .strict() because it will reject the additional keys
246
- // before the union gets a chance to validate them.
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 (additionalProperties) {
263
- if (additionalProperties.expression === "z.never()") {
264
- // Don't use .strict() if there are composition keywords that add properties
265
- output = hasCompositionKeywords ? passthroughProperties : properties + ".strict()";
266
- }
267
- else {
268
- output = properties + `.catchall(${additionalProperties.expression})`;
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
- output = passthroughProperties;
74
+ baseObjectModified = baseObjectExpr.replace(/^z\.object\(/, "z.looseObject(");
273
75
  }
274
76
  }
275
- else if (patternProperties) {
276
- output = patternProperties;
277
- }
278
- else if (additionalProperties) {
279
- output = `z.record(z.string(), ${additionalProperties.expression})`;
280
- }
281
- else {
282
- // If we have composition keywords, start with empty object instead of z.record()
283
- // The composition will provide the actual schema via .and()
284
- output = hasCompositionKeywords ? "z.object({})" : `z.record(z.string(), ${fallback.expression})`;
285
- }
286
- if (unevaluated === false && properties && !hasCompositionKeywords) {
287
- output += ".strict()";
288
- }
289
- else if (unevaluated && typeof unevaluated !== 'boolean') {
290
- const unevaluatedSchema = parseSchema(unevaluated, {
291
- ...refs,
292
- path: [...refs.path, "unevaluatedProperties"],
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
- const isKnown = ${JSON.stringify(knownKeys)}.includes(key);
301
- const matchesPattern = ${patterns.length ? "[" + patterns.map((r) => r.toString()).join(",") + "]" : "[]"}.some((r) => r.test(key));
302
- if (!isKnown && !matchesPattern) {
303
- const result = ${unevaluatedSchema.expression}.safeParse(value[key]);
304
- if (!result.success) {
305
- ctx.addIssue({ code: "custom", path: [key], message: "Invalid unevaluated property", params: { issues: result.error.issues } });
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
- // Track intersection types added via .and() calls
312
- const intersectionTypes = [];
313
- if (its.an.anyOf(objectSchema)) {
314
- const anyOfResult = parseAnyOf({
315
- ...objectSchema,
316
- anyOf: objectSchema.anyOf.map((x) => typeof x === "object" &&
317
- x !== null &&
318
- !x.type &&
319
- (x.properties || x.additionalProperties || x.patternProperties)
320
- ? { ...x, type: "object" }
321
- : x),
322
- }, refs);
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
- // Only add required validation for missing keys when there are no composition keywords
384
- // When allOf/anyOf/oneOf exist, they should define the properties and handle required validation
385
- if (missingRequiredKeys.length > 0 && !hasCompositionKeywords) {
386
- const checks = missingRequiredKeys
387
- .map((key) => `if (!Object.prototype.hasOwnProperty.call(value, ${JSON.stringify(key)})) { ctx.addIssue({ code: "custom", path: [${JSON.stringify(key)}], message: "Required property missing" }); }`)
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
- output += `.superRefine((value, ctx) => {
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
- output += `.superRefine((obj, ctx) => {
166
+ finalExpr += `.superRefine((obj, ctx) => {
422
167
  ${entries
423
168
  .map(([key, schema]) => {
424
- const parsed = parseSchema(schema, { ...refs, path: [...refs.path, "dependentSchemas", key] });
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?.dependentRequired ??
441
- "Dependent required properties missing";
442
- output += `.superRefine((obj, ctx) => {
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
- // Build the type representation from tracked property types
462
- let type;
463
- if (propertyTypes.length > 0) {
464
- // Build proper object type with actual property types
465
- const typeShape = propertyTypes
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: output,
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
- };