@gabrielbryk/json-schema-to-zod 2.14.1 → 2.15.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/CHANGELOG.md +12 -0
- package/dist/core/emitZod.js +82 -102
- package/dist/generators/generateBundle.js +16 -6
- package/dist/index.js +1 -0
- package/dist/parsers/parseAllOf.js +9 -22
- package/dist/parsers/parseAnyOf.js +5 -7
- package/dist/parsers/parseArray.js +29 -33
- package/dist/parsers/parseBoolean.js +2 -4
- package/dist/parsers/parseConst.js +2 -20
- package/dist/parsers/parseEnum.js +5 -20
- package/dist/parsers/parseIfThenElse.js +6 -8
- package/dist/parsers/parseMultipleType.js +4 -7
- package/dist/parsers/parseNot.js +6 -6
- package/dist/parsers/parseNull.js +2 -4
- package/dist/parsers/parseNullable.js +2 -4
- package/dist/parsers/parseNumber.js +25 -26
- package/dist/parsers/parseObject.js +53 -63
- package/dist/parsers/parseOneOf.js +55 -25
- package/dist/parsers/parseSchema.js +44 -36
- package/dist/parsers/parseSimpleDiscriminatedOneOf.js +3 -6
- package/dist/parsers/parseString.js +53 -58
- package/dist/types/Types.d.ts +166 -1
- package/dist/types/generators/generateBundle.d.ts +2 -2
- package/dist/types/index.d.ts +1 -0
- package/dist/types/utils/schemaRepresentation.d.ts +36 -22
- package/dist/types/utils/wrapRecursiveUnion.d.ts +2 -0
- package/dist/utils/anyOrUnknown.js +2 -3
- package/dist/utils/buildIntersectionTree.js +4 -9
- package/dist/utils/normalizeUnion.js +18 -94
- package/dist/utils/schemaRepresentation.js +496 -444
- package/dist/utils/wrapRecursiveUnion.js +27 -0
- package/open-issues.md +24 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @gabrielbryk/json-schema-to-zod
|
|
2
2
|
|
|
3
|
+
## 2.15.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- e25f255: Add configurable strategy for recursive oneOf handling, defaulting to union for recursive catchall cases to avoid Zod xor+lazy validation bugs. Also add serverless workflow e2e validation coverage.
|
|
8
|
+
|
|
9
|
+
## 2.14.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- b4460e4: Restore getter-based recursion for named properties to preserve inferred types in recursive schemas.
|
|
14
|
+
|
|
3
15
|
## 2.14.1
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/dist/core/emitZod.js
CHANGED
|
@@ -1,86 +1,63 @@
|
|
|
1
1
|
import { parseSchema } from "../parsers/parseSchema.js";
|
|
2
2
|
import { expandJsdocs } from "../utils/jsdocs.js";
|
|
3
|
-
import {
|
|
3
|
+
import { collectRefNames, emitExpression, emitType, nodeHasGetter, nodeHasLazy, } from "../utils/schemaRepresentation.js";
|
|
4
4
|
import { EsmEmitter } from "../utils/esmEmitter.js";
|
|
5
5
|
import { resolveTypeName } from "../utils/schemaNaming.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
// Only return a method chain if there actually is one (like .strict() or .meta())
|
|
59
|
-
// Don't split if the method chain is .and() since that's adding more schema, not metadata
|
|
60
|
-
if (!methodChain || methodChain.length === 0 || methodChain.startsWith(".and(")) {
|
|
61
|
-
return { base: expr, methodChain: null };
|
|
62
|
-
}
|
|
63
|
-
return { base, methodChain };
|
|
64
|
-
};
|
|
65
|
-
/**
|
|
66
|
-
* Check if an object literal has a getter at its top level (not nested).
|
|
67
|
-
*/
|
|
68
|
-
const hasTopLevelGetter = (objectLiteral) => {
|
|
69
|
-
let depth = 0;
|
|
70
|
-
for (let i = 0; i < objectLiteral.length - 4; i++) {
|
|
71
|
-
const char = objectLiteral[i];
|
|
72
|
-
if (char === "{" || char === "(" || char === "[") {
|
|
73
|
-
depth++;
|
|
74
|
-
}
|
|
75
|
-
else if (char === "}" || char === ")" || char === "]") {
|
|
76
|
-
depth--;
|
|
77
|
-
}
|
|
78
|
-
else if (depth === 1 && objectLiteral.substring(i, i + 4) === "get ") {
|
|
79
|
-
// Found "get " at depth 1 (inside the top-level object, not nested)
|
|
80
|
-
return true;
|
|
6
|
+
const splitObjectMethodChain = (node) => {
|
|
7
|
+
const chain = [];
|
|
8
|
+
let current = node;
|
|
9
|
+
while (current) {
|
|
10
|
+
switch (current.kind) {
|
|
11
|
+
case "object":
|
|
12
|
+
return {
|
|
13
|
+
base: current,
|
|
14
|
+
methodChain: chain.length ? chain.reverse().join("") : null,
|
|
15
|
+
};
|
|
16
|
+
case "readonly":
|
|
17
|
+
chain.push(".readonly()");
|
|
18
|
+
current = current.inner;
|
|
19
|
+
break;
|
|
20
|
+
case "describe":
|
|
21
|
+
chain.push(`.describe(${JSON.stringify(current.description)})`);
|
|
22
|
+
current = current.inner;
|
|
23
|
+
break;
|
|
24
|
+
case "meta":
|
|
25
|
+
chain.push(`.meta(${current.meta})`);
|
|
26
|
+
current = current.inner;
|
|
27
|
+
break;
|
|
28
|
+
case "default":
|
|
29
|
+
chain.push(`.default(${JSON.stringify(current.value)})`);
|
|
30
|
+
current = current.inner;
|
|
31
|
+
break;
|
|
32
|
+
case "catchall":
|
|
33
|
+
chain.push(`.catchall(${emitExpression(current.catchall)})`);
|
|
34
|
+
current = current.base;
|
|
35
|
+
break;
|
|
36
|
+
case "superRefine":
|
|
37
|
+
chain.push(`.superRefine(${current.refine})`);
|
|
38
|
+
current = current.base;
|
|
39
|
+
break;
|
|
40
|
+
case "refine":
|
|
41
|
+
chain.push(`.refine(${current.refine})`);
|
|
42
|
+
current = current.base;
|
|
43
|
+
break;
|
|
44
|
+
case "transform":
|
|
45
|
+
chain.push(`.transform(${current.transform})`);
|
|
46
|
+
current = current.base;
|
|
47
|
+
break;
|
|
48
|
+
case "pipe":
|
|
49
|
+
chain.push(`.pipe(${emitExpression(current.second)}${current.params ?? ""})`);
|
|
50
|
+
current = current.first;
|
|
51
|
+
break;
|
|
52
|
+
case "chain":
|
|
53
|
+
chain.push(`.${current.method}`);
|
|
54
|
+
current = current.base;
|
|
55
|
+
break;
|
|
56
|
+
default:
|
|
57
|
+
return { base: node, methodChain: null };
|
|
81
58
|
}
|
|
82
59
|
}
|
|
83
|
-
return
|
|
60
|
+
return { base: node, methodChain: null };
|
|
84
61
|
};
|
|
85
62
|
const orderDeclarations = (entries, dependencies) => {
|
|
86
63
|
const repByName = new Map(entries);
|
|
@@ -101,16 +78,18 @@ const orderDeclarations = (entries, dependencies) => {
|
|
|
101
78
|
onlyKnown.forEach((d) => current.add(d));
|
|
102
79
|
depGraph.set(from, current);
|
|
103
80
|
}
|
|
104
|
-
// Add
|
|
81
|
+
// Add dependencies from IR
|
|
105
82
|
const names = Array.from(repByName.keys());
|
|
106
83
|
for (const [name, rep] of entries) {
|
|
107
84
|
const deps = depGraph.get(name) ?? new Set();
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
85
|
+
if (!rep.node) {
|
|
86
|
+
throw new Error(`Missing IR node for ${name} (no-fallback mode).`);
|
|
87
|
+
}
|
|
88
|
+
const node = rep.node;
|
|
89
|
+
const refs = collectRefNames(node);
|
|
90
|
+
for (const refName of refs) {
|
|
91
|
+
if (refName !== name && repByName.has(refName)) {
|
|
92
|
+
deps.add(refName);
|
|
114
93
|
}
|
|
115
94
|
}
|
|
116
95
|
depGraph.set(name, deps);
|
|
@@ -196,44 +175,45 @@ export const emitZod = (analysis) => {
|
|
|
196
175
|
}
|
|
197
176
|
if (declarations.size) {
|
|
198
177
|
for (const [refName, rep] of orderDeclarations(Array.from(declarations.entries()), dependencies)) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
throw new Error(`Expected declaration expression for ${refName}`);
|
|
178
|
+
if (!rep.node) {
|
|
179
|
+
throw new Error(`Missing IR node for ${refName} (no-fallback mode).`);
|
|
202
180
|
}
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
typeof rep.type === "string"
|
|
207
|
-
? rep.type
|
|
208
|
-
: undefined;
|
|
181
|
+
const node = rep.node;
|
|
182
|
+
const expression = emitExpression(node);
|
|
183
|
+
const hintedType = rep.type;
|
|
209
184
|
const effectiveHint = hintedType === "z.ZodTypeAny" ? undefined : hintedType;
|
|
210
|
-
const hasLazy =
|
|
211
|
-
const hasGetter =
|
|
185
|
+
const hasLazy = nodeHasLazy(node);
|
|
186
|
+
const hasGetter = nodeHasGetter(node);
|
|
212
187
|
// Check if this schema references any cycle members (recursive schemas)
|
|
213
188
|
// This can cause TS7056 when TypeScript tries to serialize the expanded type
|
|
214
|
-
|
|
189
|
+
let referencesRecursiveSchema = false;
|
|
190
|
+
const refs = collectRefNames(node);
|
|
191
|
+
for (const refName of refs) {
|
|
192
|
+
if (cycleRefNames.has(refName)) {
|
|
193
|
+
referencesRecursiveSchema = true;
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
215
197
|
// Per Zod v4 docs: type annotations should be on GETTERS for recursive types, not on const declarations.
|
|
216
198
|
// TypeScript can infer the type of const declarations.
|
|
217
199
|
// Exceptions that need explicit type annotation:
|
|
218
200
|
// 1. z.lazy() without getters
|
|
219
201
|
// 2. Any schema that references recursive schemas (to prevent TS7056)
|
|
220
202
|
const needsTypeAnnotation = (hasLazy && !hasGetter) || referencesRecursiveSchema;
|
|
221
|
-
const storedType = needsTypeAnnotation
|
|
222
|
-
? (effectiveHint ?? inferTypeFromExpression(expression))
|
|
223
|
-
: undefined;
|
|
203
|
+
const storedType = needsTypeAnnotation ? (effectiveHint ?? emitType(node)) : undefined;
|
|
224
204
|
// Rule 2 from Zod v4: Don't chain methods on recursive types
|
|
225
205
|
// If the schema has getters (recursive), we need to split it:
|
|
226
206
|
// 1. Emit base schema as _RefName
|
|
227
207
|
// 2. Emit decorated schema as RefName = _RefName.methods()
|
|
228
|
-
if (hasGetter
|
|
229
|
-
const { base, methodChain } = splitObjectMethodChain(
|
|
208
|
+
if (hasGetter) {
|
|
209
|
+
const { base, methodChain } = splitObjectMethodChain(node);
|
|
230
210
|
if (methodChain) {
|
|
231
211
|
// Emit base schema (internal, not exported)
|
|
232
212
|
// No type annotation needed - type is on getters, TypeScript infers the rest
|
|
233
213
|
const baseName = `_${refName}`;
|
|
234
214
|
emitter.addConst({
|
|
235
215
|
name: baseName,
|
|
236
|
-
expression: base,
|
|
216
|
+
expression: emitExpression(base),
|
|
237
217
|
exported: false,
|
|
238
218
|
});
|
|
239
219
|
// Emit decorated schema (exported)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { analyzeSchema } from "../core/analyzeSchema.js";
|
|
2
2
|
import { emitZod } from "../core/emitZod.js";
|
|
3
3
|
import { liftInlineObjects } from "../utils/liftInlineObjects.js";
|
|
4
|
+
import { anyOrUnknown } from "../utils/anyOrUnknown.js";
|
|
5
|
+
import { zodLazy, zodRef } from "../utils/schemaRepresentation.js";
|
|
4
6
|
export const generateSchemaBundle = (schema, options = {}) => {
|
|
5
7
|
if (!schema || typeof schema !== "object") {
|
|
6
8
|
throw new Error("generateSchemaBundle requires an object schema");
|
|
@@ -137,16 +139,20 @@ const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options
|
|
|
137
139
|
path: refs.path,
|
|
138
140
|
isCycle,
|
|
139
141
|
});
|
|
140
|
-
if (resolved)
|
|
142
|
+
if (resolved) {
|
|
143
|
+
if (!resolved.node) {
|
|
144
|
+
throw new Error("refResolution.onRef must return SchemaRepresentation with node (no-fallback mode).");
|
|
145
|
+
}
|
|
141
146
|
return resolved;
|
|
147
|
+
}
|
|
142
148
|
// Self-recursion ALWAYS needs z.lazy if not using getters
|
|
143
149
|
if (refName === currentDefName) {
|
|
144
|
-
return
|
|
150
|
+
return zodLazy(refInfo.schemaName);
|
|
145
151
|
}
|
|
146
152
|
if (isCycle && useLazyCrossRefs) {
|
|
147
|
-
return
|
|
153
|
+
return zodLazy(refInfo.schemaName);
|
|
148
154
|
}
|
|
149
|
-
return refInfo.schemaName;
|
|
155
|
+
return zodRef(refInfo.schemaName);
|
|
150
156
|
}
|
|
151
157
|
// If it's NOT exactly a top-level definition, it could be:
|
|
152
158
|
// 1. A path into a top-level definition (e.g. #/$defs/alpha/properties/foo)
|
|
@@ -159,9 +165,13 @@ const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options
|
|
|
159
165
|
ref: refPath,
|
|
160
166
|
currentDef: currentDefName,
|
|
161
167
|
});
|
|
162
|
-
if (unknown)
|
|
168
|
+
if (unknown) {
|
|
169
|
+
if (!unknown.node) {
|
|
170
|
+
throw new Error("refResolution.onUnknownRef must return SchemaRepresentation with node (no-fallback mode).");
|
|
171
|
+
}
|
|
163
172
|
return unknown;
|
|
164
|
-
|
|
173
|
+
}
|
|
174
|
+
return anyOrUnknown(refs);
|
|
165
175
|
}
|
|
166
176
|
return undefined;
|
|
167
177
|
};
|
package/dist/index.js
CHANGED
|
@@ -39,6 +39,7 @@ export * from "./utils/resolveUri.js";
|
|
|
39
39
|
export * from "./utils/schemaNaming.js";
|
|
40
40
|
export * from "./utils/schemaRepresentation.js";
|
|
41
41
|
export * from "./utils/withMessage.js";
|
|
42
|
+
export * from "./utils/wrapRecursiveUnion.js";
|
|
42
43
|
export * from "./zodToJsonSchema.js";
|
|
43
44
|
import { jsonSchemaToZod } from "./jsonSchemaToZod.js";
|
|
44
45
|
export default jsonSchemaToZod;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parseSchema } from "./parseSchema.js";
|
|
2
2
|
import { half } from "../utils/half.js";
|
|
3
|
+
import { shouldUseGetter, zodExactOptional, zodIntersection, zodLooseObject, zodNever, } from "../utils/schemaRepresentation.js";
|
|
3
4
|
const originalIndexKey = "__originalIndex";
|
|
4
5
|
/**
|
|
5
6
|
* Check if a schema defines object properties (inline object shape) without any refs.
|
|
@@ -22,7 +23,6 @@ const isInlineObjectOnly = (schema) => {
|
|
|
22
23
|
*/
|
|
23
24
|
const parseObjectShape = (schema, refs, pathPrefix) => {
|
|
24
25
|
const shapeEntries = [];
|
|
25
|
-
const shapeTypes = [];
|
|
26
26
|
for (const key of Object.keys(schema.properties)) {
|
|
27
27
|
const propSchema = schema.properties[key];
|
|
28
28
|
const parsedProp = parseSchema(propSchema, {
|
|
@@ -34,12 +34,11 @@ const parseObjectShape = (schema, refs, pathPrefix) => {
|
|
|
34
34
|
? schema.required.includes(key)
|
|
35
35
|
: typeof propSchema === "object" && propSchema.required === true;
|
|
36
36
|
const optional = !hasDefault && !required;
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
shapeEntries.push(
|
|
40
|
-
shapeTypes.push(`${JSON.stringify(key)}: ${valueType}`);
|
|
37
|
+
const valueRep = optional ? zodExactOptional(parsedProp) : parsedProp;
|
|
38
|
+
const isGetter = shouldUseGetter(valueRep, refs.currentSchemaName, refs.cycleRefNames, refs.cycleComponentByName);
|
|
39
|
+
shapeEntries.push({ key, rep: valueRep, isGetter });
|
|
41
40
|
}
|
|
42
|
-
return { shapeEntries
|
|
41
|
+
return { shapeEntries };
|
|
43
42
|
};
|
|
44
43
|
/**
|
|
45
44
|
* Check if all allOf members can be combined using spread syntax.
|
|
@@ -48,7 +47,6 @@ const parseObjectShape = (schema, refs, pathPrefix) => {
|
|
|
48
47
|
*/
|
|
49
48
|
const trySpreadPattern = (allOfMembers, refs) => {
|
|
50
49
|
const shapeEntries = [];
|
|
51
|
-
const shapeTypes = [];
|
|
52
50
|
for (let i = 0; i < allOfMembers.length; i++) {
|
|
53
51
|
const member = allOfMembers[i];
|
|
54
52
|
const idx = member[originalIndexKey] ?? i;
|
|
@@ -57,20 +55,12 @@ const trySpreadPattern = (allOfMembers, refs) => {
|
|
|
57
55
|
return undefined;
|
|
58
56
|
}
|
|
59
57
|
// Extract shape entries from inline object
|
|
60
|
-
const { shapeEntries: entries
|
|
61
|
-
...refs.path,
|
|
62
|
-
"allOf",
|
|
63
|
-
idx,
|
|
64
|
-
]);
|
|
58
|
+
const { shapeEntries: entries } = parseObjectShape(member, refs, [...refs.path, "allOf", idx]);
|
|
65
59
|
shapeEntries.push(...entries);
|
|
66
|
-
shapeTypes.push(...types);
|
|
67
60
|
}
|
|
68
61
|
if (shapeEntries.length === 0)
|
|
69
62
|
return undefined;
|
|
70
|
-
return
|
|
71
|
-
expression: `z.looseObject({ ${shapeEntries.join(", ")} })`,
|
|
72
|
-
type: `z.ZodObject<{ ${shapeTypes.join(", ")} }>`,
|
|
73
|
-
};
|
|
63
|
+
return zodLooseObject(shapeEntries);
|
|
74
64
|
};
|
|
75
65
|
const ensureOriginalIndex = (arr) => {
|
|
76
66
|
const newArr = [];
|
|
@@ -90,7 +80,7 @@ const ensureOriginalIndex = (arr) => {
|
|
|
90
80
|
};
|
|
91
81
|
export function parseAllOf(schema, refs) {
|
|
92
82
|
if (schema.allOf.length === 0) {
|
|
93
|
-
return
|
|
83
|
+
return zodNever();
|
|
94
84
|
}
|
|
95
85
|
else if (schema.allOf.length === 1) {
|
|
96
86
|
const item = schema.allOf[0];
|
|
@@ -116,9 +106,6 @@ export function parseAllOf(schema, refs) {
|
|
|
116
106
|
const [left, right] = half(indexed);
|
|
117
107
|
const leftResult = parseAllOf({ allOf: left }, refs);
|
|
118
108
|
const rightResult = parseAllOf({ allOf: right }, refs);
|
|
119
|
-
return
|
|
120
|
-
expression: `z.intersection(${leftResult.expression}, ${rightResult.expression})`,
|
|
121
|
-
type: `z.ZodIntersection<${leftResult.type}, ${rightResult.type}>`,
|
|
122
|
-
};
|
|
109
|
+
return zodIntersection(leftResult, rightResult);
|
|
123
110
|
}
|
|
124
111
|
}
|
|
@@ -2,6 +2,8 @@ import { parseSchema } from "./parseSchema.js";
|
|
|
2
2
|
import { anyOrUnknown } from "../utils/anyOrUnknown.js";
|
|
3
3
|
import { extractInlineObject } from "../utils/extractInlineObject.js";
|
|
4
4
|
import { normalizeUnionMembers } from "../utils/normalizeUnion.js";
|
|
5
|
+
import { wrapRecursiveUnion } from "../utils/wrapRecursiveUnion.js";
|
|
6
|
+
import { zodRef, zodUnion } from "../utils/schemaRepresentation.js";
|
|
5
7
|
export const parseAnyOf = (schema, refs) => {
|
|
6
8
|
if (!schema.anyOf.length) {
|
|
7
9
|
return anyOrUnknown(refs);
|
|
@@ -16,7 +18,7 @@ export const parseAnyOf = (schema, refs) => {
|
|
|
16
18
|
const members = schema.anyOf.map((memberSchema, i) => {
|
|
17
19
|
const extracted = extractInlineObject(memberSchema, refs, [...refs.path, "anyOf", i]);
|
|
18
20
|
if (extracted) {
|
|
19
|
-
return
|
|
21
|
+
return zodRef(extracted);
|
|
20
22
|
}
|
|
21
23
|
return parseSchema(memberSchema, { ...refs, path: [...refs.path, "anyOf", i] });
|
|
22
24
|
});
|
|
@@ -27,10 +29,6 @@ export const parseAnyOf = (schema, refs) => {
|
|
|
27
29
|
if (normalized.length === 1) {
|
|
28
30
|
return normalized[0];
|
|
29
31
|
}
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
const expression = `z.union([${expressions}])`;
|
|
33
|
-
// Use readonly tuple for union type annotations (required for recursive type inference)
|
|
34
|
-
const type = `z.ZodUnion<readonly [${types}]>`;
|
|
35
|
-
return { expression, type };
|
|
32
|
+
const union = zodUnion(normalized, { readonlyType: true });
|
|
33
|
+
return wrapRecursiveUnion(refs, union);
|
|
36
34
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { withMessage } from "../utils/withMessage.js";
|
|
2
2
|
import { parseSchema } from "./parseSchema.js";
|
|
3
3
|
import { anyOrUnknown } from "../utils/anyOrUnknown.js";
|
|
4
|
+
import { zodArray, zodChain, zodSuperRefine, zodTuple } from "../utils/schemaRepresentation.js";
|
|
4
5
|
export const parseArray = (schema, refs) => {
|
|
5
6
|
// JSON Schema 2020-12 uses `prefixItems` for tuples.
|
|
6
7
|
// Older drafts used `items` as an array.
|
|
@@ -8,33 +9,29 @@ export const parseArray = (schema, refs) => {
|
|
|
8
9
|
if (prefixItems) {
|
|
9
10
|
// Tuple case
|
|
10
11
|
const itemResults = prefixItems.map((v, i) => parseSchema(v, { ...refs, path: [...refs.path, "prefixItems", i] }));
|
|
11
|
-
let tuple = `z.tuple([${itemResults.map((r) => r.expression).join(", ")}])`;
|
|
12
|
-
// We construct the type manually for the tuple part
|
|
13
|
-
let tupleTypes = itemResults.map((r) => r.type).join(", ");
|
|
14
|
-
let tupleType = `z.ZodTuple<[${tupleTypes}], null>`; // Default null rest
|
|
15
12
|
// Handle "additionalItems" (older drafts) or "items" (2020-12 when prefixItems is used)
|
|
16
13
|
// If prefixItems is present, `items` acts as the schema for additional items.
|
|
17
14
|
// If prefixItems came from `items` (array form), then `additionalItems` controls the rest.
|
|
18
15
|
const additionalSchema = schema.prefixItems ? schema.items : schema.additionalItems;
|
|
16
|
+
let rest;
|
|
19
17
|
if (additionalSchema === false) {
|
|
20
18
|
// Closed tuple
|
|
19
|
+
rest = null;
|
|
21
20
|
}
|
|
22
21
|
else if (additionalSchema) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
tupleType = `z.ZodTuple<[${tupleTypes}], ${restSchema.type}>`;
|
|
22
|
+
rest =
|
|
23
|
+
additionalSchema === true
|
|
24
|
+
? anyOrUnknown(refs)
|
|
25
|
+
: parseSchema(additionalSchema, {
|
|
26
|
+
...refs,
|
|
27
|
+
path: [...refs.path, "items"],
|
|
28
|
+
});
|
|
31
29
|
}
|
|
32
30
|
else {
|
|
33
31
|
// Open by default
|
|
34
|
-
|
|
35
|
-
tuple += `.rest(${anyRes.expression})`;
|
|
36
|
-
tupleType = `z.ZodTuple<[${tupleTypes}], ${anyRes.type}>`;
|
|
32
|
+
rest = anyOrUnknown(refs);
|
|
37
33
|
}
|
|
34
|
+
let result = zodTuple(itemResults, rest);
|
|
38
35
|
if (schema.contains) {
|
|
39
36
|
const containsResult = parseSchema(schema.contains, {
|
|
40
37
|
...refs,
|
|
@@ -42,7 +39,7 @@ export const parseArray = (schema, refs) => {
|
|
|
42
39
|
});
|
|
43
40
|
const minContains = schema.minContains ?? (schema.contains ? 1 : undefined);
|
|
44
41
|
const maxContains = schema.maxContains;
|
|
45
|
-
|
|
42
|
+
result = zodSuperRefine(result, `(arr, ctx) => {
|
|
46
43
|
const matches = arr.filter((item) => ${containsResult.expression}.safeParse(item).success).length;
|
|
47
44
|
if (${minContains ?? 0} && matches < ${minContains ?? 0}) {
|
|
48
45
|
ctx.addIssue({ code: "custom", message: "Array contains too few matching items" });
|
|
@@ -50,12 +47,9 @@ export const parseArray = (schema, refs) => {
|
|
|
50
47
|
if (${maxContains ?? "undefined"} !== undefined && matches > ${maxContains ?? "undefined"}) {
|
|
51
48
|
ctx.addIssue({ code: "custom", message: "Array contains too many matching items" });
|
|
52
49
|
}
|
|
53
|
-
})
|
|
50
|
+
}`);
|
|
54
51
|
}
|
|
55
|
-
return
|
|
56
|
-
expression: tuple,
|
|
57
|
-
type: tupleType,
|
|
58
|
-
};
|
|
52
|
+
return result;
|
|
59
53
|
}
|
|
60
54
|
// Regular Array case
|
|
61
55
|
const itemsSchema = schema.items;
|
|
@@ -66,22 +60,27 @@ export const parseArray = (schema, refs) => {
|
|
|
66
60
|
...refs,
|
|
67
61
|
path: [...refs.path, "items"],
|
|
68
62
|
});
|
|
69
|
-
let
|
|
70
|
-
|
|
71
|
-
r += withMessage(schema, "minItems", ({ json }) => ({
|
|
63
|
+
let result = zodArray(itemResult);
|
|
64
|
+
const minItems = withMessage(schema, "minItems", ({ json }) => ({
|
|
72
65
|
opener: `.min(${json}`,
|
|
73
66
|
closer: ")",
|
|
74
67
|
messagePrefix: ", { message: ",
|
|
75
68
|
messageCloser: " })",
|
|
76
69
|
}));
|
|
77
|
-
|
|
70
|
+
if (minItems) {
|
|
71
|
+
result = zodChain(result, minItems.slice(1));
|
|
72
|
+
}
|
|
73
|
+
const maxItems = withMessage(schema, "maxItems", ({ json }) => ({
|
|
78
74
|
opener: `.max(${json}`,
|
|
79
75
|
closer: ")",
|
|
80
76
|
messagePrefix: ", { message: ",
|
|
81
77
|
messageCloser: " })",
|
|
82
78
|
}));
|
|
79
|
+
if (maxItems) {
|
|
80
|
+
result = zodChain(result, maxItems.slice(1));
|
|
81
|
+
}
|
|
83
82
|
if (schema.uniqueItems === true) {
|
|
84
|
-
|
|
83
|
+
result = zodSuperRefine(result, `(arr, ctx) => {
|
|
85
84
|
const seen = new Set();
|
|
86
85
|
for (const [index, value] of arr.entries()) {
|
|
87
86
|
let key;
|
|
@@ -102,7 +101,7 @@ export const parseArray = (schema, refs) => {
|
|
|
102
101
|
|
|
103
102
|
seen.add(key);
|
|
104
103
|
}
|
|
105
|
-
})
|
|
104
|
+
}`);
|
|
106
105
|
}
|
|
107
106
|
if (schema.contains) {
|
|
108
107
|
const containsResult = parseSchema(schema.contains, {
|
|
@@ -111,7 +110,7 @@ export const parseArray = (schema, refs) => {
|
|
|
111
110
|
});
|
|
112
111
|
const minContains = schema.minContains ?? (schema.contains ? 1 : undefined);
|
|
113
112
|
const maxContains = schema.maxContains;
|
|
114
|
-
|
|
113
|
+
result = zodSuperRefine(result, `(arr, ctx) => {
|
|
115
114
|
const matches = arr.filter((item) => ${containsResult.expression}.safeParse(item).success).length;
|
|
116
115
|
if (${minContains ?? 0} && matches < ${minContains ?? 0}) {
|
|
117
116
|
ctx.addIssue({ code: "custom", message: "Array contains too few matching items" });
|
|
@@ -119,10 +118,7 @@ export const parseArray = (schema, refs) => {
|
|
|
119
118
|
if (${maxContains ?? "undefined"} !== undefined && matches > ${maxContains ?? "undefined"}) {
|
|
120
119
|
ctx.addIssue({ code: "custom", message: "Array contains too many matching items" });
|
|
121
120
|
}
|
|
122
|
-
})
|
|
121
|
+
}`);
|
|
123
122
|
}
|
|
124
|
-
return
|
|
125
|
-
expression: r,
|
|
126
|
-
type: arrayType,
|
|
127
|
-
};
|
|
123
|
+
return result;
|
|
128
124
|
};
|
|
@@ -1,22 +1,4 @@
|
|
|
1
|
+
import { zodLiteral } from "../utils/schemaRepresentation.js";
|
|
1
2
|
export const parseConst = (schema) => {
|
|
2
|
-
|
|
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 };
|
|
3
|
+
return zodLiteral(schema.const);
|
|
22
4
|
};
|
|
@@ -1,35 +1,20 @@
|
|
|
1
|
+
import { zodEnum, zodLiteral, zodNever, zodUnion } from "../utils/schemaRepresentation.js";
|
|
1
2
|
export const parseEnum = (schema) => {
|
|
2
3
|
if (schema.enum.length === 0) {
|
|
3
|
-
return
|
|
4
|
-
expression: "z.never()",
|
|
5
|
-
type: "z.ZodNever",
|
|
6
|
-
};
|
|
4
|
+
return zodNever();
|
|
7
5
|
}
|
|
8
6
|
else if (schema.enum.length === 1) {
|
|
9
7
|
// union does not work when there is only one element
|
|
10
8
|
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
|
-
};
|
|
9
|
+
return zodLiteral(value);
|
|
15
10
|
}
|
|
16
11
|
else if (schema.enum.every((x) => typeof x === "string")) {
|
|
17
12
|
const values = schema.enum;
|
|
18
13
|
// Zod v4 ZodEnum uses object format: { key: "key"; ... }
|
|
19
|
-
|
|
20
|
-
return {
|
|
21
|
-
expression: `z.enum([${values.map((x) => JSON.stringify(x))}])`,
|
|
22
|
-
type: `z.ZodEnum<{ ${enumObject} }>`,
|
|
23
|
-
};
|
|
14
|
+
return zodEnum(values, { typeStyle: "object" });
|
|
24
15
|
}
|
|
25
16
|
else {
|
|
26
17
|
// Mixed types: create union of literals
|
|
27
|
-
|
|
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
|
-
};
|
|
18
|
+
return zodUnion(schema.enum.map((value) => zodLiteral(value)));
|
|
34
19
|
}
|
|
35
20
|
};
|