@gabrielbryk/json-schema-to-zod 2.10.0 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +44 -0
- package/CHANGELOG.md +35 -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 +225 -67
- 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 +11 -7
- 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 +168 -29
- package/dist/parsers/parseOneOf.js +365 -0
- package/dist/{esm/parsers → parsers}/parseSchema.js +56 -110
- 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 -141
- package/dist/cjs/generators/generateBundle.js +0 -365
- 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 -315
- package/dist/cjs/parsers/parseOneOf.js +0 -53
- package/dist/cjs/parsers/parseSchema.js +0 -411
- 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 -137
- 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
|
@@ -3,16 +3,20 @@ import { parseSchema } from "./parseSchema.js";
|
|
|
3
3
|
import { anyOrUnknown } from "../utils/anyOrUnknown.js";
|
|
4
4
|
export const parseArray = (schema, refs) => {
|
|
5
5
|
if (Array.isArray(schema.items)) {
|
|
6
|
-
|
|
6
|
+
// Tuple case
|
|
7
|
+
const itemResults = schema.items.map((v, i) => parseSchema(v, { ...refs, path: [...refs.path, "items", i] }));
|
|
8
|
+
let tuple = `z.tuple([${itemResults.map(r => r.expression).join(", ")}])`;
|
|
9
|
+
const tupleTypes = itemResults.map(r => r.type).join(", ");
|
|
10
|
+
let tupleType = `z.ZodTuple<[${tupleTypes}]>`;
|
|
7
11
|
if (schema.contains) {
|
|
8
|
-
const
|
|
12
|
+
const containsResult = parseSchema(schema.contains, {
|
|
9
13
|
...refs,
|
|
10
14
|
path: [...refs.path, "contains"],
|
|
11
15
|
});
|
|
12
16
|
const minContains = schema.minContains ?? (schema.contains ? 1 : undefined);
|
|
13
17
|
const maxContains = schema.maxContains;
|
|
14
18
|
tuple += `.superRefine((arr, ctx) => {
|
|
15
|
-
const matches = arr.filter((item) => ${
|
|
19
|
+
const matches = arr.filter((item) => ${containsResult.expression}.safeParse(item).success).length;
|
|
16
20
|
if (${minContains ?? 0} && matches < ${minContains ?? 0}) {
|
|
17
21
|
ctx.addIssue({ code: "custom", message: "Array contains too few matching items" });
|
|
18
22
|
}
|
|
@@ -20,15 +24,23 @@ export const parseArray = (schema, refs) => {
|
|
|
20
24
|
ctx.addIssue({ code: "custom", message: "Array contains too many matching items" });
|
|
21
25
|
}
|
|
22
26
|
})`;
|
|
27
|
+
// In Zod v4, .superRefine() doesn't change the type
|
|
23
28
|
}
|
|
24
|
-
return
|
|
29
|
+
return {
|
|
30
|
+
expression: tuple,
|
|
31
|
+
type: tupleType,
|
|
32
|
+
};
|
|
25
33
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
// Array case
|
|
35
|
+
const anyOrUnknownResult = anyOrUnknown(refs);
|
|
36
|
+
const itemResult = !schema.items
|
|
37
|
+
? anyOrUnknownResult
|
|
38
|
+
: parseSchema(schema.items, {
|
|
29
39
|
...refs,
|
|
30
40
|
path: [...refs.path, "items"],
|
|
31
|
-
})
|
|
41
|
+
});
|
|
42
|
+
let r = `z.array(${itemResult.expression})`;
|
|
43
|
+
let arrayType = `z.ZodArray<${itemResult.type}>`;
|
|
32
44
|
r += withMessage(schema, "minItems", ({ json }) => ({
|
|
33
45
|
opener: `.min(${json}`,
|
|
34
46
|
closer: ")",
|
|
@@ -66,14 +78,14 @@ export const parseArray = (schema, refs) => {
|
|
|
66
78
|
})`;
|
|
67
79
|
}
|
|
68
80
|
if (schema.contains) {
|
|
69
|
-
const
|
|
81
|
+
const containsResult = parseSchema(schema.contains, {
|
|
70
82
|
...refs,
|
|
71
83
|
path: [...refs.path, "contains"],
|
|
72
84
|
});
|
|
73
85
|
const minContains = schema.minContains ?? (schema.contains ? 1 : undefined);
|
|
74
86
|
const maxContains = schema.maxContains;
|
|
75
87
|
r += `.superRefine((arr, ctx) => {
|
|
76
|
-
const matches = arr.filter((item) => ${
|
|
88
|
+
const matches = arr.filter((item) => ${containsResult.expression}.safeParse(item).success).length;
|
|
77
89
|
if (${minContains ?? 0} && matches < ${minContains ?? 0}) {
|
|
78
90
|
ctx.addIssue({ code: "custom", message: "Array contains too few matching items" });
|
|
79
91
|
}
|
|
@@ -82,5 +94,9 @@ export const parseArray = (schema, refs) => {
|
|
|
82
94
|
}
|
|
83
95
|
})`;
|
|
84
96
|
}
|
|
85
|
-
|
|
97
|
+
// In Zod v4, .superRefine() doesn't change the type, so no wrapping needed
|
|
98
|
+
return {
|
|
99
|
+
expression: r,
|
|
100
|
+
type: arrayType,
|
|
101
|
+
};
|
|
86
102
|
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const parseConst = (schema) => {
|
|
2
|
+
const value = schema.const;
|
|
3
|
+
const expression = `z.literal(${JSON.stringify(value)})`;
|
|
4
|
+
// Determine the literal type based on the value type
|
|
5
|
+
let type;
|
|
6
|
+
if (typeof value === "string") {
|
|
7
|
+
type = `z.ZodLiteral<${JSON.stringify(value)}>`;
|
|
8
|
+
}
|
|
9
|
+
else if (typeof value === "number") {
|
|
10
|
+
type = `z.ZodLiteral<${value}>`;
|
|
11
|
+
}
|
|
12
|
+
else if (typeof value === "boolean") {
|
|
13
|
+
type = `z.ZodLiteral<${value}>`;
|
|
14
|
+
}
|
|
15
|
+
else if (value === null) {
|
|
16
|
+
type = "z.ZodLiteral<null>";
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
type = "z.ZodLiteral<unknown>";
|
|
20
|
+
}
|
|
21
|
+
return { expression, type };
|
|
22
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const parseEnum = (schema) => {
|
|
2
|
+
if (schema.enum.length === 0) {
|
|
3
|
+
return {
|
|
4
|
+
expression: "z.never()",
|
|
5
|
+
type: "z.ZodNever",
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
else if (schema.enum.length === 1) {
|
|
9
|
+
// union does not work when there is only one element
|
|
10
|
+
const value = schema.enum[0];
|
|
11
|
+
return {
|
|
12
|
+
expression: `z.literal(${JSON.stringify(value)})`,
|
|
13
|
+
type: `z.ZodLiteral<${typeof value === "string" ? JSON.stringify(value) : value}>`,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
else if (schema.enum.every((x) => typeof x === "string")) {
|
|
17
|
+
const values = schema.enum;
|
|
18
|
+
// Zod v4 ZodEnum uses object format: { key: "key"; ... }
|
|
19
|
+
const enumObject = values.map((x) => `${JSON.stringify(x)}: ${JSON.stringify(x)}`).join("; ");
|
|
20
|
+
return {
|
|
21
|
+
expression: `z.enum([${values.map((x) => JSON.stringify(x))}])`,
|
|
22
|
+
type: `z.ZodEnum<{ ${enumObject} }>`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
// Mixed types: create union of literals
|
|
27
|
+
const literalTypes = schema.enum.map((x) => typeof x === "string" ? JSON.stringify(x) : x === null ? "null" : String(x));
|
|
28
|
+
return {
|
|
29
|
+
expression: `z.union([${schema.enum
|
|
30
|
+
.map((x) => `z.literal(${JSON.stringify(x)})`)
|
|
31
|
+
.join(", ")}])`,
|
|
32
|
+
type: `z.ZodUnion<[${literalTypes.map((t) => `z.ZodLiteral<${t}>`).join(", ")}]>`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
};
|
|
@@ -9,13 +9,13 @@ export const parseIfThenElse = (schema, refs) => {
|
|
|
9
9
|
...refs,
|
|
10
10
|
path: [...refs.path, "else"],
|
|
11
11
|
});
|
|
12
|
-
let
|
|
13
|
-
const result = ${$if}.safeParse(value).success
|
|
14
|
-
? ${$then}.safeParse(value)
|
|
15
|
-
: ${$else}.safeParse(value);
|
|
12
|
+
let expression = `z.union([${$then.expression}, ${$else.expression}]).superRefine((value,ctx) => {
|
|
13
|
+
const result = ${$if.expression}.safeParse(value).success
|
|
14
|
+
? ${$then.expression}.safeParse(value)
|
|
15
|
+
: ${$else.expression}.safeParse(value);
|
|
16
16
|
if (!result.success) {
|
|
17
17
|
const issues = result.error.issues;
|
|
18
|
-
issues.forEach((issue) => ctx.addIssue(issue))
|
|
18
|
+
issues.forEach((issue) => ctx.addIssue({ ...issue }))
|
|
19
19
|
}
|
|
20
20
|
})`;
|
|
21
21
|
// Store original if/then/else for JSON Schema round-trip
|
|
@@ -25,7 +25,11 @@ export const parseIfThenElse = (schema, refs) => {
|
|
|
25
25
|
then: schema.then,
|
|
26
26
|
else: schema.else,
|
|
27
27
|
});
|
|
28
|
-
|
|
28
|
+
expression += `.meta({ __jsonSchema: { conditional: ${conditionalMeta} } })`;
|
|
29
29
|
}
|
|
30
|
-
return
|
|
30
|
+
return {
|
|
31
|
+
expression,
|
|
32
|
+
// In Zod v4, .superRefine() doesn't change the type
|
|
33
|
+
type: `z.ZodUnion<[${$then.type}, ${$else.type}]>`,
|
|
34
|
+
};
|
|
31
35
|
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { parseSchema } from "./parseSchema.js";
|
|
2
|
+
export const parseMultipleType = (schema, refs) => {
|
|
3
|
+
const schemas = schema.type.map((type) => parseSchema({ ...schema, type }, { ...refs, withoutDefaults: true }));
|
|
4
|
+
const expressions = schemas.map(s => s.expression).join(", ");
|
|
5
|
+
const types = schemas.map(s => s.type).join(", ");
|
|
6
|
+
return {
|
|
7
|
+
expression: `z.union([${expressions}])`,
|
|
8
|
+
type: `z.ZodUnion<[${types}]>`,
|
|
9
|
+
};
|
|
10
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { parseSchema } from "./parseSchema.js";
|
|
2
|
+
import { anyOrUnknown } from "../utils/anyOrUnknown.js";
|
|
3
|
+
export const parseNot = (schema, refs) => {
|
|
4
|
+
const baseSchema = anyOrUnknown(refs);
|
|
5
|
+
const notSchema = parseSchema(schema.not, {
|
|
6
|
+
...refs,
|
|
7
|
+
path: [...refs.path, "not"],
|
|
8
|
+
});
|
|
9
|
+
return {
|
|
10
|
+
expression: `${baseSchema.expression}.refine((value) => !${notSchema.expression}.safeParse(value).success, "Invalid input: Should NOT be valid against schema")`,
|
|
11
|
+
// In Zod v4, .refine() doesn't change the type
|
|
12
|
+
type: baseSchema.type,
|
|
13
|
+
};
|
|
14
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { omit } from "../utils/omit.js";
|
|
2
|
+
import { parseSchema } from "./parseSchema.js";
|
|
3
|
+
/**
|
|
4
|
+
* For compatibility with open api 3.0 nullable
|
|
5
|
+
*/
|
|
6
|
+
export const parseNullable = (schema, refs) => {
|
|
7
|
+
const innerSchema = parseSchema(omit(schema, "nullable"), refs, true);
|
|
8
|
+
return {
|
|
9
|
+
expression: `${innerSchema.expression}.nullable()`,
|
|
10
|
+
type: `z.ZodNullable<${innerSchema.type}>`,
|
|
11
|
+
};
|
|
12
|
+
};
|
|
@@ -5,8 +5,36 @@ 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
|
+
// Helper to add type: "object" to composition members that have properties but no explicit type
|
|
17
|
+
const addObjectType = (members) => members.map((x) => typeof x === "object" &&
|
|
18
|
+
x !== null &&
|
|
19
|
+
!x.type &&
|
|
20
|
+
(x.properties || x.additionalProperties || x.patternProperties)
|
|
21
|
+
? { ...x, type: "object" }
|
|
22
|
+
: x);
|
|
23
|
+
// If only allOf, delegate to parseAllOf
|
|
24
|
+
if (hasNoDirectSchema && its.an.allOf(objectSchema) && !its.an.anyOf(objectSchema) && !its.a.oneOf(objectSchema) && !its.a.conditional(objectSchema)) {
|
|
25
|
+
return parseAllOf({ ...objectSchema, allOf: addObjectType(objectSchema.allOf) }, refs);
|
|
26
|
+
}
|
|
27
|
+
// If only anyOf, delegate to parseAnyOf
|
|
28
|
+
if (hasNoDirectSchema && its.an.anyOf(objectSchema) && !its.an.allOf(objectSchema) && !its.a.oneOf(objectSchema) && !its.a.conditional(objectSchema)) {
|
|
29
|
+
return parseAnyOf({ ...objectSchema, anyOf: addObjectType(objectSchema.anyOf) }, refs);
|
|
30
|
+
}
|
|
31
|
+
// If only oneOf, delegate to parseOneOf
|
|
32
|
+
if (hasNoDirectSchema && its.a.oneOf(objectSchema) && !its.an.allOf(objectSchema) && !its.an.anyOf(objectSchema) && !its.a.conditional(objectSchema)) {
|
|
33
|
+
return parseOneOf({ ...objectSchema, oneOf: addObjectType(objectSchema.oneOf) }, refs);
|
|
34
|
+
}
|
|
9
35
|
let properties = undefined;
|
|
36
|
+
// Track property types for building proper object type annotations
|
|
37
|
+
const propertyTypes = [];
|
|
10
38
|
if (objectSchema.properties) {
|
|
11
39
|
if (!Object.keys(objectSchema.properties).length) {
|
|
12
40
|
properties = "z.object({})";
|
|
@@ -26,10 +54,18 @@ export function parseObject(objectSchema, refs) {
|
|
|
26
54
|
: typeof propSchema === "object" && propSchema.required === true;
|
|
27
55
|
const optional = !hasDefault && !required;
|
|
28
56
|
const valueWithOptional = optional
|
|
29
|
-
? `${parsedProp}.optional()`
|
|
30
|
-
: parsedProp;
|
|
31
|
-
|
|
32
|
-
|
|
57
|
+
? `${parsedProp.expression}.optional()`
|
|
58
|
+
: parsedProp.expression;
|
|
59
|
+
// Calculate the type for getters (needed for recursive type inference)
|
|
60
|
+
const valueType = optional
|
|
61
|
+
? `z.ZodOptional<${parsedProp.type}>`
|
|
62
|
+
: parsedProp.type;
|
|
63
|
+
// Track the property type for building the object type
|
|
64
|
+
propertyTypes.push({ key, type: valueType });
|
|
65
|
+
const useGetter = shouldUseGetter(valueWithOptional, refs);
|
|
66
|
+
let result = useGetter
|
|
67
|
+
// Type annotation on getter is required for recursive type inference in unions
|
|
68
|
+
? `get ${JSON.stringify(key)}(): ${valueType} { return ${valueWithOptional} }`
|
|
33
69
|
: `${JSON.stringify(key)}: ${valueWithOptional}`;
|
|
34
70
|
if (refs.withJsdocs && typeof propSchema === "object") {
|
|
35
71
|
result = addJsdocs(propSchema, result);
|
|
@@ -47,6 +83,10 @@ export function parseObject(objectSchema, refs) {
|
|
|
47
83
|
})
|
|
48
84
|
: undefined;
|
|
49
85
|
const unevaluated = objectSchema.unevaluatedProperties;
|
|
86
|
+
const definedPropertyKeys = objectSchema.properties ? Object.keys(objectSchema.properties) : [];
|
|
87
|
+
const missingRequiredKeys = Array.isArray(objectSchema.required)
|
|
88
|
+
? objectSchema.required.filter((key) => !definedPropertyKeys.includes(key))
|
|
89
|
+
: [];
|
|
50
90
|
let patternProperties = undefined;
|
|
51
91
|
if (objectSchema.patternProperties) {
|
|
52
92
|
const parsedPatternProperties = Object.fromEntries(Object.entries(objectSchema.patternProperties).map(([key, value]) => {
|
|
@@ -58,33 +98,35 @@ export function parseObject(objectSchema, refs) {
|
|
|
58
98
|
}),
|
|
59
99
|
];
|
|
60
100
|
}, {}));
|
|
101
|
+
// Helper to get expressions from parsed pattern properties
|
|
102
|
+
const patternExprs = Object.values(parsedPatternProperties).map(r => r.expression);
|
|
61
103
|
patternProperties = "";
|
|
62
104
|
if (properties) {
|
|
63
105
|
if (additionalProperties) {
|
|
64
106
|
patternProperties += `.catchall(z.union([${[
|
|
65
|
-
...
|
|
66
|
-
additionalProperties,
|
|
107
|
+
...patternExprs,
|
|
108
|
+
additionalProperties.expression,
|
|
67
109
|
].join(", ")}]))`;
|
|
68
110
|
}
|
|
69
111
|
else if (Object.keys(parsedPatternProperties).length > 1) {
|
|
70
|
-
patternProperties += `.catchall(z.union([${
|
|
112
|
+
patternProperties += `.catchall(z.union([${patternExprs.join(", ")}]))`;
|
|
71
113
|
}
|
|
72
114
|
else {
|
|
73
|
-
patternProperties += `.catchall(${
|
|
115
|
+
patternProperties += `.catchall(${patternExprs.join("")})`;
|
|
74
116
|
}
|
|
75
117
|
}
|
|
76
118
|
else {
|
|
77
119
|
if (additionalProperties) {
|
|
78
120
|
patternProperties += `z.record(z.string(), z.union([${[
|
|
79
|
-
...
|
|
80
|
-
additionalProperties,
|
|
121
|
+
...patternExprs,
|
|
122
|
+
additionalProperties.expression,
|
|
81
123
|
].join(", ")}]))`;
|
|
82
124
|
}
|
|
83
125
|
else if (Object.keys(parsedPatternProperties).length > 1) {
|
|
84
|
-
patternProperties += `z.record(z.string(), z.union([${
|
|
126
|
+
patternProperties += `z.record(z.string(), z.union([${patternExprs.join(", ")}]))`;
|
|
85
127
|
}
|
|
86
128
|
else {
|
|
87
|
-
patternProperties += `z.record(z.string(), ${
|
|
129
|
+
patternProperties += `z.record(z.string(), ${patternExprs.join("")})`;
|
|
88
130
|
}
|
|
89
131
|
}
|
|
90
132
|
patternProperties += ".superRefine((value, ctx) => {\n";
|
|
@@ -107,7 +149,7 @@ export function parseObject(objectSchema, refs) {
|
|
|
107
149
|
}
|
|
108
150
|
patternProperties +=
|
|
109
151
|
"const result = " +
|
|
110
|
-
parsedPatternProperties[key] +
|
|
152
|
+
parsedPatternProperties[key].expression +
|
|
111
153
|
".safeParse(value[key])\n";
|
|
112
154
|
patternProperties += "if (!result.success) {\n";
|
|
113
155
|
patternProperties += `ctx.addIssue({
|
|
@@ -124,7 +166,7 @@ export function parseObject(objectSchema, refs) {
|
|
|
124
166
|
if (additionalProperties) {
|
|
125
167
|
patternProperties += "if (!evaluated) {\n";
|
|
126
168
|
patternProperties +=
|
|
127
|
-
"const result = " + additionalProperties + ".safeParse(value[key])\n";
|
|
169
|
+
"const result = " + additionalProperties.expression + ".safeParse(value[key])\n";
|
|
128
170
|
patternProperties += "if (!result.success) {\n";
|
|
129
171
|
patternProperties += `ctx.addIssue({
|
|
130
172
|
path: [...(ctx.path ?? []), key],
|
|
@@ -152,22 +194,32 @@ export function parseObject(objectSchema, refs) {
|
|
|
152
194
|
// In that case, we should NOT use .strict() because it will reject the additional keys
|
|
153
195
|
// before the union gets a chance to validate them.
|
|
154
196
|
const hasCompositionKeywords = its.an.anyOf(objectSchema) || its.a.oneOf(objectSchema) || its.an.allOf(objectSchema) || its.a.conditional(objectSchema);
|
|
197
|
+
// When there are composition keywords (allOf, anyOf, oneOf, if-then-else) but no direct properties,
|
|
198
|
+
// we should NOT default to z.record(z.string(), z.any()) because that would allow any properties.
|
|
199
|
+
// Instead, use z.object({}) and let the .and() call add properties from the composition.
|
|
200
|
+
// This is especially important when unevaluatedProperties: false is set.
|
|
201
|
+
const fallback = anyOrUnknown(refs);
|
|
155
202
|
let output = properties
|
|
156
203
|
? patternProperties
|
|
157
204
|
? properties + patternProperties
|
|
158
205
|
: additionalProperties
|
|
159
|
-
? additionalProperties === "z.never()"
|
|
206
|
+
? additionalProperties.expression === "z.never()"
|
|
160
207
|
// Don't use .strict() if there are composition keywords that add properties
|
|
161
208
|
? hasCompositionKeywords
|
|
162
209
|
? properties
|
|
163
210
|
: properties + ".strict()"
|
|
164
|
-
: properties + `.catchall(${additionalProperties})`
|
|
211
|
+
: properties + `.catchall(${additionalProperties.expression})`
|
|
165
212
|
: properties
|
|
166
213
|
: patternProperties
|
|
167
214
|
? patternProperties
|
|
168
215
|
: additionalProperties
|
|
169
|
-
? `z.record(z.string(), ${additionalProperties})`
|
|
170
|
-
|
|
216
|
+
? `z.record(z.string(), ${additionalProperties.expression})`
|
|
217
|
+
// If we have composition keywords, start with empty object instead of z.record()
|
|
218
|
+
// The composition will provide the actual schema via .and()
|
|
219
|
+
: hasCompositionKeywords
|
|
220
|
+
? "z.object({})"
|
|
221
|
+
// No constraints = any object. Use z.record() which is cleaner than z.object({}).catchall()
|
|
222
|
+
: `z.record(z.string(), ${fallback.expression})`;
|
|
171
223
|
if (unevaluated === false && properties && !hasCompositionKeywords) {
|
|
172
224
|
output += ".strict()";
|
|
173
225
|
}
|
|
@@ -185,7 +237,7 @@ export function parseObject(objectSchema, refs) {
|
|
|
185
237
|
const isKnown = ${JSON.stringify(knownKeys)}.includes(key);
|
|
186
238
|
const matchesPattern = ${patterns.length ? "[" + patterns.map((r) => r.toString()).join(",") + "]" : "[]"}.some((r) => r.test(key));
|
|
187
239
|
if (!isKnown && !matchesPattern) {
|
|
188
|
-
const result = ${unevaluatedSchema}.safeParse(value[key]);
|
|
240
|
+
const result = ${unevaluatedSchema.expression}.safeParse(value[key]);
|
|
189
241
|
if (!result.success) {
|
|
190
242
|
ctx.addIssue({ code: "custom", path: [key], message: "Invalid unevaluated property", params: { issues: result.error.issues } });
|
|
191
243
|
}
|
|
@@ -193,8 +245,10 @@ export function parseObject(objectSchema, refs) {
|
|
|
193
245
|
}
|
|
194
246
|
})`;
|
|
195
247
|
}
|
|
248
|
+
// Track intersection types added via .and() calls
|
|
249
|
+
const intersectionTypes = [];
|
|
196
250
|
if (its.an.anyOf(objectSchema)) {
|
|
197
|
-
|
|
251
|
+
const anyOfResult = parseAnyOf({
|
|
198
252
|
...objectSchema,
|
|
199
253
|
anyOf: objectSchema.anyOf.map((x) => typeof x === "object" &&
|
|
200
254
|
x !== null &&
|
|
@@ -202,10 +256,12 @@ export function parseObject(objectSchema, refs) {
|
|
|
202
256
|
(x.properties || x.additionalProperties || x.patternProperties)
|
|
203
257
|
? { ...x, type: "object" }
|
|
204
258
|
: x),
|
|
205
|
-
}, refs)
|
|
259
|
+
}, refs);
|
|
260
|
+
output += `.and(${anyOfResult.expression})`;
|
|
261
|
+
intersectionTypes.push(anyOfResult.type);
|
|
206
262
|
}
|
|
207
263
|
if (its.a.oneOf(objectSchema)) {
|
|
208
|
-
|
|
264
|
+
const oneOfResult = parseOneOf({
|
|
209
265
|
...objectSchema,
|
|
210
266
|
oneOf: objectSchema.oneOf.map((x) => typeof x === "object" &&
|
|
211
267
|
x !== null &&
|
|
@@ -213,10 +269,21 @@ export function parseObject(objectSchema, refs) {
|
|
|
213
269
|
(x.properties || x.additionalProperties || x.patternProperties)
|
|
214
270
|
? { ...x, type: "object" }
|
|
215
271
|
: x),
|
|
216
|
-
}, refs)
|
|
272
|
+
}, refs);
|
|
273
|
+
// Check if this is a refinement-only result (required fields validation)
|
|
274
|
+
// If so, apply superRefine directly instead of creating an intersection
|
|
275
|
+
const resultWithRefinement = oneOfResult;
|
|
276
|
+
if (resultWithRefinement.isRefinementOnly && resultWithRefinement.refinementBody) {
|
|
277
|
+
output += `.superRefine(${resultWithRefinement.refinementBody})`;
|
|
278
|
+
// No intersection type needed - superRefine doesn't change the type
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
output += `.and(${oneOfResult.expression})`;
|
|
282
|
+
intersectionTypes.push(oneOfResult.type);
|
|
283
|
+
}
|
|
217
284
|
}
|
|
218
285
|
if (its.an.allOf(objectSchema)) {
|
|
219
|
-
|
|
286
|
+
const allOfResult = parseAllOf({
|
|
220
287
|
...objectSchema,
|
|
221
288
|
allOf: objectSchema.allOf.map((x) => typeof x === "object" &&
|
|
222
289
|
x !== null &&
|
|
@@ -224,11 +291,23 @@ export function parseObject(objectSchema, refs) {
|
|
|
224
291
|
(x.properties || x.additionalProperties || x.patternProperties)
|
|
225
292
|
? { ...x, type: "object" }
|
|
226
293
|
: x),
|
|
227
|
-
}, refs)
|
|
294
|
+
}, refs);
|
|
295
|
+
output += `.and(${allOfResult.expression})`;
|
|
296
|
+
intersectionTypes.push(allOfResult.type);
|
|
228
297
|
}
|
|
229
298
|
// Handle if/then/else conditionals on object schemas
|
|
230
299
|
if (its.a.conditional(objectSchema)) {
|
|
231
|
-
|
|
300
|
+
const conditionalResult = parseIfThenElse(objectSchema, refs);
|
|
301
|
+
output += `.and(${conditionalResult.expression})`;
|
|
302
|
+
intersectionTypes.push(conditionalResult.type);
|
|
303
|
+
}
|
|
304
|
+
// Only add required validation for missing keys when there are no composition keywords
|
|
305
|
+
// When allOf/anyOf/oneOf exist, they should define the properties and handle required validation
|
|
306
|
+
if (missingRequiredKeys.length > 0 && !hasCompositionKeywords) {
|
|
307
|
+
const checks = missingRequiredKeys
|
|
308
|
+
.map((key) => `if (!Object.prototype.hasOwnProperty.call(value, ${JSON.stringify(key)})) { ctx.addIssue({ code: "custom", path: [${JSON.stringify(key)}], message: "Required property missing" }); }`)
|
|
309
|
+
.join(" ");
|
|
310
|
+
output += `.superRefine((value, ctx) => { if (value && typeof value === "object") { ${checks} } })`;
|
|
232
311
|
}
|
|
233
312
|
// propertyNames
|
|
234
313
|
if (objectSchema.propertyNames) {
|
|
@@ -300,13 +379,73 @@ export function parseObject(objectSchema, refs) {
|
|
|
300
379
|
})`;
|
|
301
380
|
}
|
|
302
381
|
}
|
|
303
|
-
|
|
382
|
+
// Build the type representation from tracked property types
|
|
383
|
+
let type;
|
|
384
|
+
if (propertyTypes.length > 0) {
|
|
385
|
+
// Build proper object type with actual property types
|
|
386
|
+
const typeShape = propertyTypes
|
|
387
|
+
.map(({ key, type: propType }) => `${JSON.stringify(key)}: ${propType}`)
|
|
388
|
+
.join("; ");
|
|
389
|
+
type = `z.ZodObject<{ ${typeShape} }>`;
|
|
390
|
+
}
|
|
391
|
+
else if (properties === "z.object({})") {
|
|
392
|
+
// Empty object
|
|
393
|
+
type = "z.ZodObject<{}>";
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
// Fallback for complex cases (patternProperties, record, etc.)
|
|
397
|
+
type = inferTypeFromExpression(output);
|
|
398
|
+
}
|
|
399
|
+
// Wrap in intersection types if .and() calls were added
|
|
400
|
+
for (const intersectionType of intersectionTypes) {
|
|
401
|
+
type = `z.ZodIntersection<${type}, ${intersectionType}>`;
|
|
402
|
+
}
|
|
403
|
+
return {
|
|
404
|
+
expression: output,
|
|
405
|
+
type,
|
|
406
|
+
};
|
|
304
407
|
}
|
|
408
|
+
/**
|
|
409
|
+
* Determines if a property should use getter syntax for recursive references.
|
|
410
|
+
* Getters defer evaluation until access time, which is the Zod v4 recommended
|
|
411
|
+
* approach for handling recursive schemas in object properties.
|
|
412
|
+
*/
|
|
305
413
|
const shouldUseGetter = (parsed, refs) => {
|
|
306
414
|
if (!parsed)
|
|
307
415
|
return false;
|
|
308
|
-
|
|
416
|
+
// Check for z.lazy() - these should use getters
|
|
417
|
+
if (parsed.includes("z.lazy("))
|
|
309
418
|
return true;
|
|
419
|
+
// Check for direct self-recursion (expression contains the current schema name)
|
|
420
|
+
// This handles cases like generateSchemaBundle where the schema name is different
|
|
421
|
+
// from the def name (e.g., NodeSchema vs node)
|
|
422
|
+
if (refs.currentSchemaName) {
|
|
423
|
+
const selfRefPattern = new RegExp(`\\b${refs.currentSchemaName}\\b`);
|
|
424
|
+
if (selfRefPattern.test(parsed)) {
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
// Check for direct recursive references in the same SCC
|
|
429
|
+
if (refs.currentSchemaName && refs.cycleRefNames && refs.cycleComponentByName) {
|
|
430
|
+
const cycleRefNames = refs.cycleRefNames;
|
|
431
|
+
const cycleComponentByName = refs.cycleComponentByName;
|
|
432
|
+
const refNameArray = Array.from(cycleRefNames);
|
|
433
|
+
// Check if expression contains a reference to a cycle member in the same component
|
|
434
|
+
if (containsRecursiveRef(parsed, cycleRefNames)) {
|
|
435
|
+
const currentComponent = cycleComponentByName.get(refs.currentSchemaName);
|
|
436
|
+
if (currentComponent !== undefined) {
|
|
437
|
+
for (let i = 0; i < refNameArray.length; i++) {
|
|
438
|
+
const refName = refNameArray[i];
|
|
439
|
+
const pattern = new RegExp(`\\b${refName}\\b`);
|
|
440
|
+
if (pattern.test(parsed)) {
|
|
441
|
+
const refComponent = cycleComponentByName.get(refName);
|
|
442
|
+
if (refComponent === currentComponent) {
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
310
449
|
}
|
|
311
|
-
return
|
|
450
|
+
return false;
|
|
312
451
|
};
|