@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
|
@@ -1,36 +1,50 @@
|
|
|
1
1
|
import { analyzeSchema } from "../core/analyzeSchema.js";
|
|
2
2
|
import { emitZod } from "../core/emitZod.js";
|
|
3
|
+
import { liftInlineObjects } from "../utils/liftInlineObjects.js";
|
|
3
4
|
export const generateSchemaBundle = (schema, options = {}) => {
|
|
4
|
-
const module = options.module ?? "esm";
|
|
5
5
|
if (!schema || typeof schema !== "object") {
|
|
6
6
|
throw new Error("generateSchemaBundle requires an object schema");
|
|
7
7
|
}
|
|
8
|
-
const
|
|
9
|
-
const
|
|
8
|
+
const liftOpts = options.liftInlineObjects ?? {};
|
|
9
|
+
const useLift = liftOpts.enable !== false;
|
|
10
|
+
const liftedSchema = useLift
|
|
11
|
+
? liftInlineObjects(schema, {
|
|
12
|
+
enable: true,
|
|
13
|
+
nameForPath: liftOpts.nameForPath,
|
|
14
|
+
parentName: options.splitDefs?.rootTypeName ?? options.splitDefs?.rootName ?? schema.title,
|
|
15
|
+
dedup: liftOpts.dedup === true,
|
|
16
|
+
allowInDefs: liftOpts.allowInDefs,
|
|
17
|
+
}).schema
|
|
18
|
+
: schema;
|
|
19
|
+
const defs = liftedSchema.$defs || {};
|
|
20
|
+
const definitions = liftedSchema.definitions || {};
|
|
10
21
|
const defNames = Object.keys(defs);
|
|
11
22
|
const { rootName, rootTypeName, defInfoMap } = buildBundleContext(defNames, defs, options);
|
|
12
23
|
const files = [];
|
|
13
|
-
const targets = planBundleTargets(
|
|
24
|
+
const targets = planBundleTargets(liftedSchema, defs, definitions, defNames, options, rootName, defInfoMap, rootTypeName);
|
|
14
25
|
for (const target of targets) {
|
|
15
26
|
const usedRefs = target.usedRefs;
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
const zodParts = [];
|
|
28
|
+
for (const member of target.members) {
|
|
29
|
+
const analysis = analyzeSchema(member.schemaWithDefs, {
|
|
30
|
+
...options,
|
|
31
|
+
name: member.schemaName,
|
|
32
|
+
type: member.typeName,
|
|
33
|
+
parserOverride: createRefHandler(member.defName, defInfoMap, usedRefs, {
|
|
34
|
+
...(member.schemaWithDefs.$defs || {}),
|
|
35
|
+
...(member.schemaWithDefs.definitions || {}),
|
|
36
|
+
}, options, target.groupId),
|
|
37
|
+
});
|
|
38
|
+
const zodSchema = emitZod(analysis);
|
|
39
|
+
zodParts.push(zodSchema);
|
|
40
|
+
}
|
|
41
|
+
const finalSchema = buildSchemaFile(zodParts, usedRefs, defInfoMap);
|
|
28
42
|
files.push({ fileName: target.fileName, contents: finalSchema });
|
|
29
43
|
}
|
|
30
44
|
// Nested types extraction (optional)
|
|
31
45
|
const nestedTypesEnabled = options.nestedTypes?.enable;
|
|
32
46
|
if (nestedTypesEnabled) {
|
|
33
|
-
const nestedTypes = collectNestedTypes(
|
|
47
|
+
const nestedTypes = collectNestedTypes(liftedSchema, defs, defNames, rootTypeName ?? rootName);
|
|
34
48
|
if (nestedTypes.length > 0) {
|
|
35
49
|
const nestedFileName = options.nestedTypes?.fileName ?? "nested-types.ts";
|
|
36
50
|
const nestedContent = generateNestedTypesFile(nestedTypes);
|
|
@@ -53,13 +67,16 @@ const buildDefInfoMap = (defNames, defs, options) => {
|
|
|
53
67
|
const pascalName = toPascalCase(defName);
|
|
54
68
|
const schemaName = options.splitDefs?.schemaName?.(defName, { isRoot: false }) ?? `${pascalName}Schema`;
|
|
55
69
|
const typeName = options.splitDefs?.typeName?.(defName, { isRoot: false }) ?? pascalName;
|
|
70
|
+
const fileName = options.splitDefs?.fileName?.(defName, { isRoot: false }) ?? `${defName}.schema.ts`;
|
|
56
71
|
map.set(defName, {
|
|
57
72
|
name: defName,
|
|
58
73
|
pascalName,
|
|
59
74
|
schemaName,
|
|
60
75
|
typeName,
|
|
76
|
+
fileName,
|
|
61
77
|
dependencies,
|
|
62
78
|
hasCycle: false,
|
|
79
|
+
groupId: "",
|
|
63
80
|
});
|
|
64
81
|
}
|
|
65
82
|
return map;
|
|
@@ -72,13 +89,21 @@ const buildBundleContext = (defNames, defs, options) => {
|
|
|
72
89
|
if (info)
|
|
73
90
|
info.hasCycle = true;
|
|
74
91
|
}
|
|
92
|
+
const groups = buildSccGroups(defInfoMap);
|
|
93
|
+
for (const [groupId, members] of groups) {
|
|
94
|
+
for (const defName of members) {
|
|
95
|
+
const info = defInfoMap.get(defName);
|
|
96
|
+
if (info)
|
|
97
|
+
info.groupId = groupId;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
75
100
|
const rootName = options.splitDefs?.rootName ?? options.name ?? "RootSchema";
|
|
76
101
|
const rootTypeName = typeof options.type === "string"
|
|
77
102
|
? options.type
|
|
78
103
|
: options.splitDefs?.rootTypeName ?? (typeof options.type === "boolean" && options.type ? rootName : undefined);
|
|
79
|
-
return { defInfoMap, rootName, rootTypeName };
|
|
104
|
+
return { defInfoMap, rootName, rootTypeName, groups };
|
|
80
105
|
};
|
|
81
|
-
const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options) => {
|
|
106
|
+
const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options, currentGroupId) => {
|
|
82
107
|
const useLazyCrossRefs = options.refResolution?.lazyCrossRefs ?? true;
|
|
83
108
|
return (schema, refs) => {
|
|
84
109
|
if (typeof schema["$ref"] === "string") {
|
|
@@ -93,7 +118,7 @@ const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options
|
|
|
93
118
|
const refInfo = defInfoMap.get(refName);
|
|
94
119
|
if (refInfo) {
|
|
95
120
|
// Track imports when referencing other defs
|
|
96
|
-
if (refName !== currentDefName) {
|
|
121
|
+
if (refName !== currentDefName && refInfo.groupId !== currentGroupId) {
|
|
97
122
|
usedRefs.add(refName);
|
|
98
123
|
}
|
|
99
124
|
const isCycle = refName === currentDefName || (refInfo.hasCycle && !!currentDefName);
|
|
@@ -107,7 +132,14 @@ const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options
|
|
|
107
132
|
if (resolved)
|
|
108
133
|
return resolved;
|
|
109
134
|
if (isCycle && useLazyCrossRefs) {
|
|
110
|
-
|
|
135
|
+
const inObjectProperty = refs.path.includes("properties") ||
|
|
136
|
+
refs.path.includes("patternProperties") ||
|
|
137
|
+
refs.path.includes("additionalProperties");
|
|
138
|
+
if (inObjectProperty && refName === currentDefName) {
|
|
139
|
+
// Self-recursion inside object getters can safely reference the schema name
|
|
140
|
+
return refInfo.schemaName;
|
|
141
|
+
}
|
|
142
|
+
return `z.lazy<typeof ${refInfo.schemaName}>(() => ${refInfo.schemaName})`;
|
|
111
143
|
}
|
|
112
144
|
return refInfo.schemaName;
|
|
113
145
|
}
|
|
@@ -125,42 +157,72 @@ const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options
|
|
|
125
157
|
return undefined;
|
|
126
158
|
};
|
|
127
159
|
};
|
|
128
|
-
const buildSchemaFile = (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
160
|
+
const buildSchemaFile = (zodCodeParts, usedRefs, defInfoMap) => {
|
|
161
|
+
const groupFileById = new Map();
|
|
162
|
+
for (const info of defInfoMap.values()) {
|
|
163
|
+
if (!groupFileById.has(info.groupId)) {
|
|
164
|
+
groupFileById.set(info.groupId, info.fileName.replace(/\.ts$/, ".js"));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const importsByFile = new Map();
|
|
132
168
|
for (const refName of [...usedRefs].sort()) {
|
|
133
169
|
const refInfo = defInfoMap.get(refName);
|
|
134
170
|
if (refInfo) {
|
|
135
|
-
|
|
171
|
+
const groupFile = groupFileById.get(refInfo.groupId) ?? refInfo.fileName.replace(/\.ts$/, ".js");
|
|
172
|
+
const path = `./${groupFile}`;
|
|
173
|
+
const set = importsByFile.get(path) ?? new Set();
|
|
174
|
+
set.add(refInfo.schemaName);
|
|
175
|
+
importsByFile.set(path, set);
|
|
136
176
|
}
|
|
137
177
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
178
|
+
const imports = [];
|
|
179
|
+
for (const [path, names] of [...importsByFile.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
180
|
+
imports.push(`import { ${[...names].sort().join(", ")} } from '${path}';`);
|
|
181
|
+
}
|
|
182
|
+
const body = zodCodeParts
|
|
183
|
+
.map((code, idx) => {
|
|
184
|
+
if (idx === 0)
|
|
185
|
+
return code;
|
|
186
|
+
return code.replace(/^import \{ z \} from "zod"\n?/, "");
|
|
187
|
+
})
|
|
188
|
+
.join("\n");
|
|
189
|
+
const withImports = imports.length
|
|
190
|
+
? body.replace('import { z } from "zod"', `import { z } from "zod"\n${imports.join("\n")}`)
|
|
191
|
+
: body;
|
|
192
|
+
return withImports;
|
|
141
193
|
};
|
|
142
|
-
const planBundleTargets = (rootSchema, defs, definitions, defNames, options, rootName, rootTypeName) => {
|
|
194
|
+
const planBundleTargets = (rootSchema, defs, definitions, defNames, options, rootName, defInfoMap, rootTypeName) => {
|
|
143
195
|
const targets = [];
|
|
196
|
+
const groupById = new Map();
|
|
144
197
|
for (const defName of defNames) {
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
198
|
+
const info = defInfoMap.get(defName);
|
|
199
|
+
const gid = info?.groupId || defName;
|
|
200
|
+
if (!groupById.has(gid))
|
|
201
|
+
groupById.set(gid, []);
|
|
202
|
+
groupById.get(gid).push(defName);
|
|
203
|
+
}
|
|
204
|
+
for (const [groupId, memberDefs] of groupById.entries()) {
|
|
205
|
+
const orderedDefs = orderGroupMembers(memberDefs, defInfoMap);
|
|
206
|
+
const members = orderedDefs.map((defName) => {
|
|
207
|
+
const defSchema = defs[defName];
|
|
208
|
+
const defSchemaWithDefs = {
|
|
209
|
+
...defSchema,
|
|
210
|
+
$defs: { ...defs, ...defSchema?.$defs },
|
|
211
|
+
definitions: {
|
|
212
|
+
...defSchema.definitions,
|
|
213
|
+
...definitions,
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
const pascalName = toPascalCase(defName);
|
|
217
|
+
const schemaName = options.splitDefs?.schemaName?.(defName, { isRoot: false }) ?? `${pascalName}Schema`;
|
|
218
|
+
const typeName = options.splitDefs?.typeName?.(defName, { isRoot: false }) ?? pascalName;
|
|
219
|
+
return { defName, schemaWithDefs: defSchemaWithDefs, schemaName, typeName };
|
|
220
|
+
});
|
|
221
|
+
const fileName = defInfoMap.get(memberDefs[0])?.fileName ?? `${memberDefs[0]}.schema.ts`;
|
|
158
222
|
targets.push({
|
|
159
|
-
|
|
160
|
-
schemaWithDefs: defSchemaWithDefs,
|
|
161
|
-
schemaName,
|
|
162
|
-
typeName,
|
|
223
|
+
groupId,
|
|
163
224
|
fileName,
|
|
225
|
+
members,
|
|
164
226
|
usedRefs: new Set(),
|
|
165
227
|
isRoot: false,
|
|
166
228
|
});
|
|
@@ -168,17 +230,22 @@ const planBundleTargets = (rootSchema, defs, definitions, defNames, options, roo
|
|
|
168
230
|
if (options.splitDefs?.includeRoot ?? true) {
|
|
169
231
|
const rootFile = options.splitDefs?.fileName?.("root", { isRoot: true }) ?? "workflow.schema.ts";
|
|
170
232
|
targets.push({
|
|
171
|
-
|
|
172
|
-
schemaWithDefs: {
|
|
173
|
-
...rootSchema,
|
|
174
|
-
definitions: {
|
|
175
|
-
...rootSchema.definitions,
|
|
176
|
-
...definitions,
|
|
177
|
-
},
|
|
178
|
-
},
|
|
179
|
-
schemaName: rootName,
|
|
180
|
-
typeName: rootTypeName,
|
|
233
|
+
groupId: "root",
|
|
181
234
|
fileName: rootFile,
|
|
235
|
+
members: [
|
|
236
|
+
{
|
|
237
|
+
defName: null,
|
|
238
|
+
schemaWithDefs: {
|
|
239
|
+
...rootSchema,
|
|
240
|
+
definitions: {
|
|
241
|
+
...rootSchema.definitions,
|
|
242
|
+
...definitions,
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
schemaName: rootName,
|
|
246
|
+
typeName: rootTypeName,
|
|
247
|
+
},
|
|
248
|
+
],
|
|
182
249
|
usedRefs: new Set(),
|
|
183
250
|
isRoot: true,
|
|
184
251
|
});
|
|
@@ -187,9 +254,13 @@ const planBundleTargets = (rootSchema, defs, definitions, defNames, options, roo
|
|
|
187
254
|
};
|
|
188
255
|
const findRefDependencies = (schema, validDefNames) => {
|
|
189
256
|
const deps = new Set();
|
|
257
|
+
const seen = new WeakSet();
|
|
190
258
|
function traverse(obj) {
|
|
191
259
|
if (obj === null || typeof obj !== "object")
|
|
192
260
|
return;
|
|
261
|
+
if (seen.has(obj))
|
|
262
|
+
return;
|
|
263
|
+
seen.add(obj);
|
|
193
264
|
if (Array.isArray(obj)) {
|
|
194
265
|
obj.forEach(traverse);
|
|
195
266
|
return;
|
|
@@ -209,6 +280,35 @@ const findRefDependencies = (schema, validDefNames) => {
|
|
|
209
280
|
traverse(schema);
|
|
210
281
|
return deps;
|
|
211
282
|
};
|
|
283
|
+
const orderGroupMembers = (defs, defInfoMap) => {
|
|
284
|
+
const inGroup = new Set(defs);
|
|
285
|
+
const visited = new Set();
|
|
286
|
+
const temp = new Set();
|
|
287
|
+
const result = [];
|
|
288
|
+
const visit = (name) => {
|
|
289
|
+
if (visited.has(name))
|
|
290
|
+
return;
|
|
291
|
+
if (temp.has(name)) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
temp.add(name);
|
|
295
|
+
const info = defInfoMap.get(name);
|
|
296
|
+
if (info) {
|
|
297
|
+
for (const dep of info.dependencies) {
|
|
298
|
+
if (inGroup.has(dep)) {
|
|
299
|
+
visit(dep);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
temp.delete(name);
|
|
304
|
+
visited.add(name);
|
|
305
|
+
result.push(name);
|
|
306
|
+
};
|
|
307
|
+
for (const name of defs) {
|
|
308
|
+
visit(name);
|
|
309
|
+
}
|
|
310
|
+
return result;
|
|
311
|
+
};
|
|
212
312
|
const detectCycles = (defInfoMap) => {
|
|
213
313
|
const cycleNodes = new Set();
|
|
214
314
|
const visited = new Set();
|
|
@@ -242,6 +342,52 @@ const detectCycles = (defInfoMap) => {
|
|
|
242
342
|
}
|
|
243
343
|
return cycleNodes;
|
|
244
344
|
};
|
|
345
|
+
const buildSccGroups = (defInfoMap) => {
|
|
346
|
+
const indexMap = new Map();
|
|
347
|
+
const lowLink = new Map();
|
|
348
|
+
const onStack = new Set();
|
|
349
|
+
const stack = [];
|
|
350
|
+
let index = 0;
|
|
351
|
+
const groups = new Map();
|
|
352
|
+
const strongConnect = (node) => {
|
|
353
|
+
indexMap.set(node, index);
|
|
354
|
+
lowLink.set(node, index);
|
|
355
|
+
index += 1;
|
|
356
|
+
stack.push(node);
|
|
357
|
+
onStack.add(node);
|
|
358
|
+
const info = defInfoMap.get(node);
|
|
359
|
+
if (info) {
|
|
360
|
+
for (const dep of info.dependencies) {
|
|
361
|
+
if (!indexMap.has(dep)) {
|
|
362
|
+
strongConnect(dep);
|
|
363
|
+
lowLink.set(node, Math.min(lowLink.get(node), lowLink.get(dep)));
|
|
364
|
+
}
|
|
365
|
+
else if (onStack.has(dep)) {
|
|
366
|
+
lowLink.set(node, Math.min(lowLink.get(node), indexMap.get(dep)));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (lowLink.get(node) === indexMap.get(node)) {
|
|
371
|
+
const members = [];
|
|
372
|
+
let w;
|
|
373
|
+
do {
|
|
374
|
+
w = stack.pop();
|
|
375
|
+
if (w) {
|
|
376
|
+
onStack.delete(w);
|
|
377
|
+
members.push(w);
|
|
378
|
+
}
|
|
379
|
+
} while (w && w !== node);
|
|
380
|
+
const groupId = members.sort().join("__");
|
|
381
|
+
groups.set(groupId, members);
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
for (const name of defInfoMap.keys()) {
|
|
385
|
+
if (!indexMap.has(name)) {
|
|
386
|
+
strongConnect(name);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return groups;
|
|
390
|
+
};
|
|
245
391
|
const collectNestedTypes = (rootSchema, defs, defNames, rootTypeName) => {
|
|
246
392
|
const allNestedTypes = [];
|
|
247
393
|
for (const defName of defNames) {
|
|
@@ -317,6 +463,26 @@ const generateNestedTypesFile = (nestedTypes) => {
|
|
|
317
463
|
" * They are extracted using TypeScript indexed access types.",
|
|
318
464
|
" */",
|
|
319
465
|
"",
|
|
466
|
+
"type Access<T, P extends readonly (string | number)[]> =",
|
|
467
|
+
" P extends []",
|
|
468
|
+
" ? NonNullable<T>",
|
|
469
|
+
" : P extends readonly [infer H, ...infer R]",
|
|
470
|
+
" ? H extends \"items\"",
|
|
471
|
+
" ? Access<NonNullable<T> extends Array<infer U> ? U : unknown, Extract<R, (string | number)[]>>",
|
|
472
|
+
" : H extends \"additionalProperties\"",
|
|
473
|
+
" ? Access<NonNullable<T> extends Record<string, infer V> ? V : unknown, Extract<R, (string | number)[]>>",
|
|
474
|
+
" : H extends number",
|
|
475
|
+
" ? Access<NonNullable<T> extends Array<infer U> ? U : unknown, Extract<R, (string | number)[]>>",
|
|
476
|
+
" : H extends string",
|
|
477
|
+
" ? Access<",
|
|
478
|
+
" H extends keyof NonNullable<T>",
|
|
479
|
+
" ? NonNullable<NonNullable<T>[H]>",
|
|
480
|
+
" : unknown,",
|
|
481
|
+
" Extract<R, (string | number)[]>",
|
|
482
|
+
" >",
|
|
483
|
+
" : unknown",
|
|
484
|
+
" : unknown;",
|
|
485
|
+
"",
|
|
320
486
|
];
|
|
321
487
|
const byParent = new Map();
|
|
322
488
|
for (const info of nestedTypes) {
|
|
@@ -336,16 +502,8 @@ const generateNestedTypesFile = (nestedTypes) => {
|
|
|
336
502
|
}
|
|
337
503
|
lines.push("");
|
|
338
504
|
const buildAccessExpr = (parentType, propertyPath) => {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const accessor = prop === "items"
|
|
342
|
-
? "[number]"
|
|
343
|
-
: typeof prop === "number"
|
|
344
|
-
? `[${prop}]`
|
|
345
|
-
: `[${JSON.stringify(prop)}]`;
|
|
346
|
-
accessExpr = `NonNullable<${accessExpr}${accessor}>`;
|
|
347
|
-
}
|
|
348
|
-
return accessExpr;
|
|
505
|
+
const path = propertyPath.map((prop) => (typeof prop === "number" ? prop : JSON.stringify(prop))).join(", ");
|
|
506
|
+
return `Access<${parentType}, [${path}]>`;
|
|
349
507
|
};
|
|
350
508
|
for (const [parentType, types] of [...byParent.entries()].sort()) {
|
|
351
509
|
lines.push(`// From ${parentType}`);
|
|
@@ -24,10 +24,16 @@ export * from "./parsers/parseString.js";
|
|
|
24
24
|
export * from "./utils/anyOrUnknown.js";
|
|
25
25
|
export * from "./utils/buildRefRegistry.js";
|
|
26
26
|
export * from "./utils/cycles.js";
|
|
27
|
+
export * from "./utils/esmEmitter.js";
|
|
28
|
+
export * from "./utils/extractInlineObject.js";
|
|
27
29
|
export * from "./utils/half.js";
|
|
28
30
|
export * from "./utils/jsdocs.js";
|
|
31
|
+
export * from "./utils/liftInlineObjects.js";
|
|
32
|
+
export * from "./utils/namingService.js";
|
|
29
33
|
export * from "./utils/omit.js";
|
|
34
|
+
export * from "./utils/resolveRef.js";
|
|
30
35
|
export * from "./utils/resolveUri.js";
|
|
36
|
+
export * from "./utils/schemaRepresentation.js";
|
|
31
37
|
export * from "./utils/withMessage.js";
|
|
32
38
|
export * from "./zodToJsonSchema.js";
|
|
33
39
|
import { jsonSchemaToZod } from "./jsonSchemaToZod.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { analyzeSchema } from "./core/analyzeSchema.js";
|
|
2
|
+
import { emitZod } from "./core/emitZod.js";
|
|
3
|
+
import { liftInlineObjects } from "./utils/liftInlineObjects.js";
|
|
4
|
+
export const jsonSchemaToZod = (schema, options = {}) => {
|
|
5
|
+
const liftOpts = options.liftInlineObjects ?? {};
|
|
6
|
+
const sourceSchema = liftOpts.enable !== false
|
|
7
|
+
? liftInlineObjects(schema, {
|
|
8
|
+
enable: true,
|
|
9
|
+
nameForPath: liftOpts.nameForPath,
|
|
10
|
+
parentName: options.name,
|
|
11
|
+
dedup: liftOpts.dedup === true,
|
|
12
|
+
allowInDefs: liftOpts.allowInDefs,
|
|
13
|
+
}).schema
|
|
14
|
+
: schema;
|
|
15
|
+
const analysis = analyzeSchema(sourceSchema, options);
|
|
16
|
+
return emitZod(analysis);
|
|
17
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { parseSchema } from "./parseSchema.js";
|
|
2
|
+
import { half } from "../utils/half.js";
|
|
3
|
+
const originalIndexKey = "__originalIndex";
|
|
4
|
+
/**
|
|
5
|
+
* Check if a schema defines object properties (inline object shape) without any refs.
|
|
6
|
+
*/
|
|
7
|
+
const isInlineObjectOnly = (schema) => {
|
|
8
|
+
if (typeof schema !== "object" || schema === null)
|
|
9
|
+
return false;
|
|
10
|
+
const obj = schema;
|
|
11
|
+
// Must have properties
|
|
12
|
+
if (!obj.properties || Object.keys(obj.properties).length === 0)
|
|
13
|
+
return false;
|
|
14
|
+
// Must NOT have $ref or $dynamicRef (can't use spread with refs)
|
|
15
|
+
if (obj.$ref || obj.$dynamicRef)
|
|
16
|
+
return false;
|
|
17
|
+
return true;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Parse just the shape entries from an object schema (without z.object wrapper).
|
|
21
|
+
* Returns array of "key: expression" strings for spreading.
|
|
22
|
+
*/
|
|
23
|
+
const parseObjectShape = (schema, refs, pathPrefix) => {
|
|
24
|
+
const shapeEntries = [];
|
|
25
|
+
const shapeTypes = [];
|
|
26
|
+
for (const key of Object.keys(schema.properties)) {
|
|
27
|
+
const propSchema = schema.properties[key];
|
|
28
|
+
const parsedProp = parseSchema(propSchema, {
|
|
29
|
+
...refs,
|
|
30
|
+
path: [...pathPrefix, "properties", key],
|
|
31
|
+
});
|
|
32
|
+
const hasDefault = typeof propSchema === "object" && propSchema.default !== undefined;
|
|
33
|
+
const required = Array.isArray(schema.required)
|
|
34
|
+
? schema.required.includes(key)
|
|
35
|
+
: typeof propSchema === "object" && propSchema.required === true;
|
|
36
|
+
const optional = !hasDefault && !required;
|
|
37
|
+
const valueExpr = optional
|
|
38
|
+
? `${parsedProp.expression}.optional()`
|
|
39
|
+
: parsedProp.expression;
|
|
40
|
+
const valueType = optional
|
|
41
|
+
? `z.ZodOptional<${parsedProp.type}>`
|
|
42
|
+
: parsedProp.type;
|
|
43
|
+
shapeEntries.push(`${JSON.stringify(key)}: ${valueExpr}`);
|
|
44
|
+
shapeTypes.push(`${JSON.stringify(key)}: ${valueType}`);
|
|
45
|
+
}
|
|
46
|
+
return { shapeEntries, shapeTypes };
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Check if all allOf members can be combined using spread syntax.
|
|
50
|
+
* Only works when ALL members are inline objects (no $refs).
|
|
51
|
+
* Returns the merged object if possible, undefined otherwise.
|
|
52
|
+
*/
|
|
53
|
+
const trySpreadPattern = (allOfMembers, refs) => {
|
|
54
|
+
const shapeEntries = [];
|
|
55
|
+
const shapeTypes = [];
|
|
56
|
+
for (let i = 0; i < allOfMembers.length; i++) {
|
|
57
|
+
const member = allOfMembers[i];
|
|
58
|
+
const idx = member[originalIndexKey] ?? i;
|
|
59
|
+
// Only handle pure inline objects - no refs allowed
|
|
60
|
+
if (!isInlineObjectOnly(member)) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
// Extract shape entries from inline object
|
|
64
|
+
const { shapeEntries: entries, shapeTypes: types } = parseObjectShape(member, refs, [...refs.path, "allOf", idx]);
|
|
65
|
+
shapeEntries.push(...entries);
|
|
66
|
+
shapeTypes.push(...types);
|
|
67
|
+
}
|
|
68
|
+
if (shapeEntries.length === 0)
|
|
69
|
+
return undefined;
|
|
70
|
+
return {
|
|
71
|
+
expression: `z.object({ ${shapeEntries.join(", ")} })`,
|
|
72
|
+
type: `z.ZodObject<{ ${shapeTypes.join(", ")} }>`,
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
const ensureOriginalIndex = (arr) => {
|
|
76
|
+
const newArr = [];
|
|
77
|
+
for (let i = 0; i < arr.length; i++) {
|
|
78
|
+
const item = arr[i];
|
|
79
|
+
if (typeof item === "boolean") {
|
|
80
|
+
newArr.push(item ? { [originalIndexKey]: i } : { [originalIndexKey]: i, not: {} });
|
|
81
|
+
}
|
|
82
|
+
else if (typeof item === "object" &&
|
|
83
|
+
item !== null &&
|
|
84
|
+
originalIndexKey in item) {
|
|
85
|
+
return arr;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
newArr.push({ ...item, [originalIndexKey]: i });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return newArr;
|
|
92
|
+
};
|
|
93
|
+
export function parseAllOf(schema, refs) {
|
|
94
|
+
if (schema.allOf.length === 0) {
|
|
95
|
+
return { expression: "z.never()", type: "z.ZodNever" };
|
|
96
|
+
}
|
|
97
|
+
else if (schema.allOf.length === 1) {
|
|
98
|
+
const item = schema.allOf[0];
|
|
99
|
+
return parseSchema(item, {
|
|
100
|
+
...refs,
|
|
101
|
+
path: [
|
|
102
|
+
...refs.path,
|
|
103
|
+
"allOf",
|
|
104
|
+
item[originalIndexKey] ?? 0,
|
|
105
|
+
],
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
// Try spread pattern first (more efficient than intersection)
|
|
110
|
+
// This works when all members are either $refs to object schemas or inline objects
|
|
111
|
+
const indexed = ensureOriginalIndex(schema.allOf);
|
|
112
|
+
const spreadResult = trySpreadPattern(indexed, refs);
|
|
113
|
+
if (spreadResult) {
|
|
114
|
+
return spreadResult;
|
|
115
|
+
}
|
|
116
|
+
// Fallback to intersection-based approach
|
|
117
|
+
const [left, right] = half(indexed);
|
|
118
|
+
const leftResult = parseAllOf({ allOf: left }, refs);
|
|
119
|
+
const rightResult = parseAllOf({ allOf: right }, refs);
|
|
120
|
+
return {
|
|
121
|
+
expression: `z.intersection(${leftResult.expression}, ${rightResult.expression})`,
|
|
122
|
+
type: `z.ZodIntersection<${leftResult.type}, ${rightResult.type}>`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { parseSchema } from "./parseSchema.js";
|
|
2
|
+
import { anyOrUnknown } from "../utils/anyOrUnknown.js";
|
|
3
|
+
import { extractInlineObject } from "../utils/extractInlineObject.js";
|
|
4
|
+
export const parseAnyOf = (schema, refs) => {
|
|
5
|
+
if (!schema.anyOf.length) {
|
|
6
|
+
return anyOrUnknown(refs);
|
|
7
|
+
}
|
|
8
|
+
if (schema.anyOf.length === 1) {
|
|
9
|
+
return parseSchema(schema.anyOf[0], {
|
|
10
|
+
...refs,
|
|
11
|
+
path: [...refs.path, "anyOf", 0],
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
// Rule 1: Extract inline objects to top-level declarations
|
|
15
|
+
const members = schema.anyOf.map((memberSchema, i) => {
|
|
16
|
+
const extracted = extractInlineObject(memberSchema, refs, [...refs.path, "anyOf", i]);
|
|
17
|
+
if (extracted) {
|
|
18
|
+
return { expression: extracted, type: `typeof ${extracted}` };
|
|
19
|
+
}
|
|
20
|
+
return parseSchema(memberSchema, { ...refs, path: [...refs.path, "anyOf", i] });
|
|
21
|
+
});
|
|
22
|
+
const expressions = members.map(m => m.expression).join(", ");
|
|
23
|
+
const types = members.map(m => m.type).join(", ");
|
|
24
|
+
const expression = `z.union([${expressions}])`;
|
|
25
|
+
// Use readonly tuple for union type annotations (required for recursive type inference)
|
|
26
|
+
const type = `z.ZodUnion<readonly [${types}]>`;
|
|
27
|
+
return { expression, type };
|
|
28
|
+
};
|