@gabrielbryk/json-schema-to-zod 2.9.0 → 2.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/cjs/core/emitZod.js +21 -5
- package/dist/cjs/generators/generateBundle.js +213 -58
- package/dist/cjs/parsers/parseAllOf.js +12 -6
- package/dist/cjs/parsers/parseBoolean.js +1 -3
- package/dist/cjs/parsers/parseIfThenElse.js +2 -2
- package/dist/cjs/parsers/parseNull.js +1 -3
- package/dist/cjs/parsers/parseObject.js +11 -3
- package/dist/cjs/parsers/parseSchema.js +18 -6
- package/dist/cjs/parsers/parseString.js +1 -1
- package/dist/cjs/utils/buildRefRegistry.js +1 -1
- package/dist/cjs/utils/omit.js +3 -2
- package/dist/esm/core/emitZod.js +21 -5
- package/dist/esm/generators/generateBundle.js +213 -58
- package/dist/esm/parsers/parseAllOf.js +12 -6
- package/dist/esm/parsers/parseBoolean.js +1 -3
- package/dist/esm/parsers/parseIfThenElse.js +2 -2
- package/dist/esm/parsers/parseNull.js +1 -3
- package/dist/esm/parsers/parseObject.js +11 -3
- package/dist/esm/parsers/parseSchema.js +18 -6
- package/dist/esm/parsers/parseString.js +1 -1
- package/dist/esm/utils/buildRefRegistry.js +1 -1
- package/dist/esm/utils/omit.js +3 -2
- package/dist/types/Types.d.ts +6 -3
- package/dist/types/parsers/parseBoolean.d.ts +1 -4
- package/dist/types/parsers/parseNull.d.ts +1 -4
- package/dist/types/utils/buildRefRegistry.d.ts +2 -2
- package/eslint.config.js +1 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.buildRefRegistry = void 0;
|
|
4
4
|
const resolveUri_js_1 = require("./resolveUri.js");
|
|
5
|
-
const buildRefRegistry = (schema, rootBaseUri = "root:///"
|
|
5
|
+
const buildRefRegistry = (schema, rootBaseUri = "root:///") => {
|
|
6
6
|
const registry = new Map();
|
|
7
7
|
const walk = (node, baseUri, path) => {
|
|
8
8
|
if (typeof node !== "object" || node === null)
|
package/dist/cjs/utils/omit.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.omit = void 0;
|
|
4
4
|
const omit = (obj, ...keys) => Object.keys(obj).reduce((acc, key) => {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
const typedKey = key;
|
|
6
|
+
if (!keys.includes(typedKey)) {
|
|
7
|
+
acc[typedKey] = obj[typedKey];
|
|
7
8
|
}
|
|
8
9
|
return acc;
|
|
9
10
|
}, {});
|
package/dist/esm/core/emitZod.js
CHANGED
|
@@ -70,6 +70,16 @@ export const emitZod = (analysis) => {
|
|
|
70
70
|
const { module, name, type, noImport, exportRefs, withMeta, ...rest } = options;
|
|
71
71
|
const declarations = new Map();
|
|
72
72
|
const dependencies = new Map();
|
|
73
|
+
const reserveName = (base) => {
|
|
74
|
+
let candidate = base;
|
|
75
|
+
let i = 1;
|
|
76
|
+
while (usedNames.has(candidate) || declarations.has(candidate)) {
|
|
77
|
+
candidate = `${base}${i}`;
|
|
78
|
+
i += 1;
|
|
79
|
+
}
|
|
80
|
+
usedNames.add(candidate);
|
|
81
|
+
return candidate;
|
|
82
|
+
};
|
|
73
83
|
const parsedSchema = parseSchema(schema, {
|
|
74
84
|
module,
|
|
75
85
|
name,
|
|
@@ -91,15 +101,21 @@ export const emitZod = (analysis) => {
|
|
|
91
101
|
});
|
|
92
102
|
const declarationBlock = declarations.size
|
|
93
103
|
? orderDeclarations(Array.from(declarations.entries()), dependencies)
|
|
94
|
-
.
|
|
104
|
+
.flatMap(([refName, value]) => {
|
|
95
105
|
const shouldExport = exportRefs && module === "esm";
|
|
96
|
-
const
|
|
97
|
-
|
|
106
|
+
const isCycle = cycleRefNames.has(refName);
|
|
107
|
+
if (!isCycle) {
|
|
108
|
+
return [`${shouldExport ? "export " : ""}const ${refName} = ${value}`];
|
|
109
|
+
}
|
|
110
|
+
const baseName = `${refName}Def`;
|
|
111
|
+
const lines = [`const ${baseName} = ${value}`];
|
|
112
|
+
lines.push(`${shouldExport ? "export " : ""}const ${refName} = ${baseName}`);
|
|
113
|
+
return lines;
|
|
98
114
|
})
|
|
99
115
|
.join("\n")
|
|
100
116
|
: "";
|
|
101
|
-
const jsdocs = rest.withJsdocs && typeof schema
|
|
102
|
-
? expandJsdocs(schema.description)
|
|
117
|
+
const jsdocs = rest.withJsdocs && typeof schema === "object" && schema !== null && "description" in schema
|
|
118
|
+
? expandJsdocs(String(schema.description ?? ""))
|
|
103
119
|
: "";
|
|
104
120
|
const lines = [];
|
|
105
121
|
if (module === "cjs" && !noImport) {
|
|
@@ -8,23 +8,27 @@ export const generateSchemaBundle = (schema, options = {}) => {
|
|
|
8
8
|
const defs = schema.$defs || {};
|
|
9
9
|
const definitions = schema.definitions || {};
|
|
10
10
|
const defNames = Object.keys(defs);
|
|
11
|
-
const { rootName, rootTypeName, defInfoMap } = buildBundleContext(defNames, defs, options);
|
|
11
|
+
const { rootName, rootTypeName, defInfoMap, groups } = buildBundleContext(defNames, defs, options);
|
|
12
12
|
const files = [];
|
|
13
|
-
const targets = planBundleTargets(schema, defs, definitions, defNames, options, rootName, rootTypeName);
|
|
13
|
+
const targets = planBundleTargets(schema, defs, definitions, defNames, options, rootName, defInfoMap, rootTypeName);
|
|
14
14
|
for (const target of targets) {
|
|
15
15
|
const usedRefs = target.usedRefs;
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
16
|
+
const zodParts = [];
|
|
17
|
+
for (const member of target.members) {
|
|
18
|
+
const analysis = analyzeSchema(member.schemaWithDefs, {
|
|
19
|
+
...options,
|
|
20
|
+
module,
|
|
21
|
+
name: member.schemaName,
|
|
22
|
+
type: member.typeName,
|
|
23
|
+
parserOverride: createRefHandler(member.defName, defInfoMap, usedRefs, {
|
|
24
|
+
...(member.schemaWithDefs.$defs || {}),
|
|
25
|
+
...(member.schemaWithDefs.definitions || {}),
|
|
26
|
+
}, options, target.groupId),
|
|
27
|
+
});
|
|
28
|
+
const zodSchema = emitZod(analysis);
|
|
29
|
+
zodParts.push(zodSchema);
|
|
30
|
+
}
|
|
31
|
+
const finalSchema = buildSchemaFile(zodParts, usedRefs, defInfoMap, module, target);
|
|
28
32
|
files.push({ fileName: target.fileName, contents: finalSchema });
|
|
29
33
|
}
|
|
30
34
|
// Nested types extraction (optional)
|
|
@@ -53,13 +57,16 @@ const buildDefInfoMap = (defNames, defs, options) => {
|
|
|
53
57
|
const pascalName = toPascalCase(defName);
|
|
54
58
|
const schemaName = options.splitDefs?.schemaName?.(defName, { isRoot: false }) ?? `${pascalName}Schema`;
|
|
55
59
|
const typeName = options.splitDefs?.typeName?.(defName, { isRoot: false }) ?? pascalName;
|
|
60
|
+
const fileName = options.splitDefs?.fileName?.(defName, { isRoot: false }) ?? `${defName}.schema.ts`;
|
|
56
61
|
map.set(defName, {
|
|
57
62
|
name: defName,
|
|
58
63
|
pascalName,
|
|
59
64
|
schemaName,
|
|
60
65
|
typeName,
|
|
66
|
+
fileName,
|
|
61
67
|
dependencies,
|
|
62
68
|
hasCycle: false,
|
|
69
|
+
groupId: "",
|
|
63
70
|
});
|
|
64
71
|
}
|
|
65
72
|
return map;
|
|
@@ -72,13 +79,22 @@ const buildBundleContext = (defNames, defs, options) => {
|
|
|
72
79
|
if (info)
|
|
73
80
|
info.hasCycle = true;
|
|
74
81
|
}
|
|
82
|
+
const groups = buildSccGroups(defInfoMap);
|
|
83
|
+
for (const [groupId, members] of groups) {
|
|
84
|
+
for (const defName of members) {
|
|
85
|
+
const info = defInfoMap.get(defName);
|
|
86
|
+
if (info)
|
|
87
|
+
info.groupId = groupId;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
75
90
|
const rootName = options.splitDefs?.rootName ?? options.name ?? "RootSchema";
|
|
76
91
|
const rootTypeName = typeof options.type === "string"
|
|
77
92
|
? options.type
|
|
78
93
|
: options.splitDefs?.rootTypeName ?? (typeof options.type === "boolean" && options.type ? rootName : undefined);
|
|
79
|
-
return { defInfoMap, rootName, rootTypeName };
|
|
94
|
+
return { defInfoMap, rootName, rootTypeName, groups };
|
|
80
95
|
};
|
|
81
|
-
const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options) => {
|
|
96
|
+
const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options, currentGroupId) => {
|
|
97
|
+
const useLazyCrossRefs = options.refResolution?.lazyCrossRefs ?? true;
|
|
82
98
|
return (schema, refs) => {
|
|
83
99
|
if (typeof schema["$ref"] === "string") {
|
|
84
100
|
const refPath = schema["$ref"];
|
|
@@ -92,7 +108,7 @@ const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options
|
|
|
92
108
|
const refInfo = defInfoMap.get(refName);
|
|
93
109
|
if (refInfo) {
|
|
94
110
|
// Track imports when referencing other defs
|
|
95
|
-
if (refName !== currentDefName) {
|
|
111
|
+
if (refName !== currentDefName && refInfo.groupId !== currentGroupId) {
|
|
96
112
|
usedRefs.add(refName);
|
|
97
113
|
}
|
|
98
114
|
const isCycle = refName === currentDefName || (refInfo.hasCycle && !!currentDefName);
|
|
@@ -105,7 +121,14 @@ const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options
|
|
|
105
121
|
});
|
|
106
122
|
if (resolved)
|
|
107
123
|
return resolved;
|
|
108
|
-
if (isCycle &&
|
|
124
|
+
if (isCycle && useLazyCrossRefs) {
|
|
125
|
+
const inObjectProperty = refs.path.includes("properties") ||
|
|
126
|
+
refs.path.includes("patternProperties") ||
|
|
127
|
+
refs.path.includes("additionalProperties");
|
|
128
|
+
if (inObjectProperty && refName === currentDefName) {
|
|
129
|
+
// Self-recursion inside object getters can safely reference the schema name
|
|
130
|
+
return refInfo.schemaName;
|
|
131
|
+
}
|
|
109
132
|
return `z.lazy(() => ${refInfo.schemaName})`;
|
|
110
133
|
}
|
|
111
134
|
return refInfo.schemaName;
|
|
@@ -124,42 +147,73 @@ const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options
|
|
|
124
147
|
return undefined;
|
|
125
148
|
};
|
|
126
149
|
};
|
|
127
|
-
const buildSchemaFile = (
|
|
150
|
+
const buildSchemaFile = (zodCodeParts, usedRefs, defInfoMap, module, target) => {
|
|
128
151
|
if (module !== "esm")
|
|
129
|
-
return
|
|
130
|
-
const
|
|
152
|
+
return zodCodeParts.join("\n");
|
|
153
|
+
const groupFileById = new Map();
|
|
154
|
+
for (const info of defInfoMap.values()) {
|
|
155
|
+
if (!groupFileById.has(info.groupId)) {
|
|
156
|
+
groupFileById.set(info.groupId, info.fileName.replace(/\.ts$/, ".js"));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const importsByFile = new Map();
|
|
131
160
|
for (const refName of [...usedRefs].sort()) {
|
|
132
161
|
const refInfo = defInfoMap.get(refName);
|
|
133
162
|
if (refInfo) {
|
|
134
|
-
|
|
163
|
+
const groupFile = groupFileById.get(refInfo.groupId) ?? refInfo.fileName.replace(/\.ts$/, ".js");
|
|
164
|
+
const path = `./${groupFile}`;
|
|
165
|
+
const set = importsByFile.get(path) ?? new Set();
|
|
166
|
+
set.add(refInfo.schemaName);
|
|
167
|
+
importsByFile.set(path, set);
|
|
135
168
|
}
|
|
136
169
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
170
|
+
const imports = [];
|
|
171
|
+
for (const [path, names] of [...importsByFile.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
172
|
+
imports.push(`import { ${[...names].sort().join(", ")} } from '${path}';`);
|
|
173
|
+
}
|
|
174
|
+
const body = zodCodeParts
|
|
175
|
+
.map((code, idx) => {
|
|
176
|
+
if (idx === 0)
|
|
177
|
+
return code;
|
|
178
|
+
return code.replace(/^import \{ z \} from "zod"\n?/, "");
|
|
179
|
+
})
|
|
180
|
+
.join("\n");
|
|
181
|
+
return imports.length
|
|
182
|
+
? body.replace('import { z } from "zod"', `import { z } from "zod"\n${imports.join("\n")}`)
|
|
183
|
+
: body;
|
|
140
184
|
};
|
|
141
|
-
const planBundleTargets = (rootSchema, defs, definitions, defNames, options, rootName, rootTypeName) => {
|
|
185
|
+
const planBundleTargets = (rootSchema, defs, definitions, defNames, options, rootName, defInfoMap, rootTypeName) => {
|
|
142
186
|
const targets = [];
|
|
187
|
+
const groupById = new Map();
|
|
143
188
|
for (const defName of defNames) {
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
189
|
+
const info = defInfoMap.get(defName);
|
|
190
|
+
const gid = info?.groupId || defName;
|
|
191
|
+
if (!groupById.has(gid))
|
|
192
|
+
groupById.set(gid, []);
|
|
193
|
+
groupById.get(gid).push(defName);
|
|
194
|
+
}
|
|
195
|
+
for (const [groupId, memberDefs] of groupById.entries()) {
|
|
196
|
+
const orderedDefs = orderGroupMembers(memberDefs, defInfoMap);
|
|
197
|
+
const members = orderedDefs.map((defName) => {
|
|
198
|
+
const defSchema = defs[defName];
|
|
199
|
+
const defSchemaWithDefs = {
|
|
200
|
+
...defSchema,
|
|
201
|
+
$defs: { ...defs, ...defSchema?.$defs },
|
|
202
|
+
definitions: {
|
|
203
|
+
...defSchema.definitions,
|
|
204
|
+
...definitions,
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
const pascalName = toPascalCase(defName);
|
|
208
|
+
const schemaName = options.splitDefs?.schemaName?.(defName, { isRoot: false }) ?? `${pascalName}Schema`;
|
|
209
|
+
const typeName = options.splitDefs?.typeName?.(defName, { isRoot: false }) ?? pascalName;
|
|
210
|
+
return { defName, schemaWithDefs: defSchemaWithDefs, schemaName, typeName };
|
|
211
|
+
});
|
|
212
|
+
const fileName = defInfoMap.get(memberDefs[0])?.fileName ?? `${memberDefs[0]}.schema.ts`;
|
|
157
213
|
targets.push({
|
|
158
|
-
|
|
159
|
-
schemaWithDefs: defSchemaWithDefs,
|
|
160
|
-
schemaName,
|
|
161
|
-
typeName,
|
|
214
|
+
groupId,
|
|
162
215
|
fileName,
|
|
216
|
+
members,
|
|
163
217
|
usedRefs: new Set(),
|
|
164
218
|
isRoot: false,
|
|
165
219
|
});
|
|
@@ -167,17 +221,22 @@ const planBundleTargets = (rootSchema, defs, definitions, defNames, options, roo
|
|
|
167
221
|
if (options.splitDefs?.includeRoot ?? true) {
|
|
168
222
|
const rootFile = options.splitDefs?.fileName?.("root", { isRoot: true }) ?? "workflow.schema.ts";
|
|
169
223
|
targets.push({
|
|
170
|
-
|
|
171
|
-
schemaWithDefs: {
|
|
172
|
-
...rootSchema,
|
|
173
|
-
definitions: {
|
|
174
|
-
...rootSchema.definitions,
|
|
175
|
-
...definitions,
|
|
176
|
-
},
|
|
177
|
-
},
|
|
178
|
-
schemaName: rootName,
|
|
179
|
-
typeName: rootTypeName,
|
|
224
|
+
groupId: "root",
|
|
180
225
|
fileName: rootFile,
|
|
226
|
+
members: [
|
|
227
|
+
{
|
|
228
|
+
defName: null,
|
|
229
|
+
schemaWithDefs: {
|
|
230
|
+
...rootSchema,
|
|
231
|
+
definitions: {
|
|
232
|
+
...rootSchema.definitions,
|
|
233
|
+
...definitions,
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
schemaName: rootName,
|
|
237
|
+
typeName: rootTypeName,
|
|
238
|
+
},
|
|
239
|
+
],
|
|
181
240
|
usedRefs: new Set(),
|
|
182
241
|
isRoot: true,
|
|
183
242
|
});
|
|
@@ -208,6 +267,35 @@ const findRefDependencies = (schema, validDefNames) => {
|
|
|
208
267
|
traverse(schema);
|
|
209
268
|
return deps;
|
|
210
269
|
};
|
|
270
|
+
const orderGroupMembers = (defs, defInfoMap) => {
|
|
271
|
+
const inGroup = new Set(defs);
|
|
272
|
+
const visited = new Set();
|
|
273
|
+
const temp = new Set();
|
|
274
|
+
const result = [];
|
|
275
|
+
const visit = (name) => {
|
|
276
|
+
if (visited.has(name))
|
|
277
|
+
return;
|
|
278
|
+
if (temp.has(name)) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
temp.add(name);
|
|
282
|
+
const info = defInfoMap.get(name);
|
|
283
|
+
if (info) {
|
|
284
|
+
for (const dep of info.dependencies) {
|
|
285
|
+
if (inGroup.has(dep)) {
|
|
286
|
+
visit(dep);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
temp.delete(name);
|
|
291
|
+
visited.add(name);
|
|
292
|
+
result.push(name);
|
|
293
|
+
};
|
|
294
|
+
for (const name of defs) {
|
|
295
|
+
visit(name);
|
|
296
|
+
}
|
|
297
|
+
return result;
|
|
298
|
+
};
|
|
211
299
|
const detectCycles = (defInfoMap) => {
|
|
212
300
|
const cycleNodes = new Set();
|
|
213
301
|
const visited = new Set();
|
|
@@ -241,6 +329,52 @@ const detectCycles = (defInfoMap) => {
|
|
|
241
329
|
}
|
|
242
330
|
return cycleNodes;
|
|
243
331
|
};
|
|
332
|
+
const buildSccGroups = (defInfoMap) => {
|
|
333
|
+
const indexMap = new Map();
|
|
334
|
+
const lowLink = new Map();
|
|
335
|
+
const onStack = new Set();
|
|
336
|
+
const stack = [];
|
|
337
|
+
let index = 0;
|
|
338
|
+
const groups = new Map();
|
|
339
|
+
const strongConnect = (node) => {
|
|
340
|
+
indexMap.set(node, index);
|
|
341
|
+
lowLink.set(node, index);
|
|
342
|
+
index += 1;
|
|
343
|
+
stack.push(node);
|
|
344
|
+
onStack.add(node);
|
|
345
|
+
const info = defInfoMap.get(node);
|
|
346
|
+
if (info) {
|
|
347
|
+
for (const dep of info.dependencies) {
|
|
348
|
+
if (!indexMap.has(dep)) {
|
|
349
|
+
strongConnect(dep);
|
|
350
|
+
lowLink.set(node, Math.min(lowLink.get(node), lowLink.get(dep)));
|
|
351
|
+
}
|
|
352
|
+
else if (onStack.has(dep)) {
|
|
353
|
+
lowLink.set(node, Math.min(lowLink.get(node), indexMap.get(dep)));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (lowLink.get(node) === indexMap.get(node)) {
|
|
358
|
+
const members = [];
|
|
359
|
+
let w;
|
|
360
|
+
do {
|
|
361
|
+
w = stack.pop();
|
|
362
|
+
if (w) {
|
|
363
|
+
onStack.delete(w);
|
|
364
|
+
members.push(w);
|
|
365
|
+
}
|
|
366
|
+
} while (w && w !== node);
|
|
367
|
+
const groupId = members.sort().join("__");
|
|
368
|
+
groups.set(groupId, members);
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
for (const name of defInfoMap.keys()) {
|
|
372
|
+
if (!indexMap.has(name)) {
|
|
373
|
+
strongConnect(name);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return groups;
|
|
377
|
+
};
|
|
244
378
|
const collectNestedTypes = (rootSchema, defs, defNames, rootTypeName) => {
|
|
245
379
|
const allNestedTypes = [];
|
|
246
380
|
for (const defName of defNames) {
|
|
@@ -285,7 +419,7 @@ const findNestedTypesInSchema = (schema, parentTypeName, defNames, currentPath =
|
|
|
285
419
|
}
|
|
286
420
|
// inline $defs
|
|
287
421
|
if (record.$defs && typeof record.$defs === "object") {
|
|
288
|
-
for (const [
|
|
422
|
+
for (const [, defSchema] of Object.entries(record.$defs)) {
|
|
289
423
|
nestedTypes.push(...findNestedTypesInSchema(defSchema, parentTypeName, defNames, currentPath));
|
|
290
424
|
}
|
|
291
425
|
}
|
|
@@ -316,6 +450,26 @@ const generateNestedTypesFile = (nestedTypes) => {
|
|
|
316
450
|
" * They are extracted using TypeScript indexed access types.",
|
|
317
451
|
" */",
|
|
318
452
|
"",
|
|
453
|
+
"type Access<T, P extends readonly (string | number)[]> =",
|
|
454
|
+
" P extends []",
|
|
455
|
+
" ? NonNullable<T>",
|
|
456
|
+
" : P extends readonly [infer H, ...infer R]",
|
|
457
|
+
" ? H extends \"items\"",
|
|
458
|
+
" ? Access<NonNullable<T> extends Array<infer U> ? U : unknown, Extract<R, (string | number)[]>>",
|
|
459
|
+
" : H extends \"additionalProperties\"",
|
|
460
|
+
" ? Access<NonNullable<T> extends Record<string, infer V> ? V : unknown, Extract<R, (string | number)[]>>",
|
|
461
|
+
" : H extends number",
|
|
462
|
+
" ? Access<NonNullable<T> extends Array<infer U> ? U : unknown, Extract<R, (string | number)[]>>",
|
|
463
|
+
" : H extends string",
|
|
464
|
+
" ? Access<",
|
|
465
|
+
" H extends keyof NonNullable<T>",
|
|
466
|
+
" ? NonNullable<NonNullable<T>[H]>",
|
|
467
|
+
" : unknown,",
|
|
468
|
+
" Extract<R, (string | number)[]>",
|
|
469
|
+
" >",
|
|
470
|
+
" : unknown",
|
|
471
|
+
" : unknown;",
|
|
472
|
+
"",
|
|
319
473
|
];
|
|
320
474
|
const byParent = new Map();
|
|
321
475
|
for (const info of nestedTypes) {
|
|
@@ -334,14 +488,15 @@ const generateNestedTypesFile = (nestedTypes) => {
|
|
|
334
488
|
lines.push(`import type { ${typeName} } from './${file}.schema.js';`);
|
|
335
489
|
}
|
|
336
490
|
lines.push("");
|
|
491
|
+
const buildAccessExpr = (parentType, propertyPath) => {
|
|
492
|
+
const path = propertyPath.map((prop) => (typeof prop === "number" ? prop : JSON.stringify(prop))).join(", ");
|
|
493
|
+
return `Access<${parentType}, [${path}]>`;
|
|
494
|
+
};
|
|
337
495
|
for (const [parentType, types] of [...byParent.entries()].sort()) {
|
|
338
496
|
lines.push(`// From ${parentType}`);
|
|
339
497
|
for (const info of types.sort((a, b) => a.typeName.localeCompare(b.typeName))) {
|
|
340
498
|
if (info.propertyPath.length > 0) {
|
|
341
|
-
|
|
342
|
-
for (const prop of info.propertyPath) {
|
|
343
|
-
accessExpr = `NonNullable<${accessExpr}['${prop}']>`;
|
|
344
|
-
}
|
|
499
|
+
const accessExpr = buildAccessExpr(parentType, info.propertyPath);
|
|
345
500
|
lines.push(`export type ${info.typeName} = ${accessExpr};`);
|
|
346
501
|
}
|
|
347
502
|
}
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { parseSchema } from "./parseSchema.js";
|
|
2
2
|
import { half } from "../utils/half.js";
|
|
3
|
-
const
|
|
3
|
+
const originalIndexKey = "__originalIndex";
|
|
4
4
|
const ensureOriginalIndex = (arr) => {
|
|
5
|
-
|
|
5
|
+
const newArr = [];
|
|
6
6
|
for (let i = 0; i < arr.length; i++) {
|
|
7
7
|
const item = arr[i];
|
|
8
8
|
if (typeof item === "boolean") {
|
|
9
|
-
newArr.push(item ? { [
|
|
9
|
+
newArr.push(item ? { [originalIndexKey]: i } : { [originalIndexKey]: i, not: {} });
|
|
10
10
|
}
|
|
11
|
-
else if (
|
|
11
|
+
else if (typeof item === "object" &&
|
|
12
|
+
item !== null &&
|
|
13
|
+
originalIndexKey in item) {
|
|
12
14
|
return arr;
|
|
13
15
|
}
|
|
14
16
|
else {
|
|
15
|
-
newArr.push({ ...item, [
|
|
17
|
+
newArr.push({ ...item, [originalIndexKey]: i });
|
|
16
18
|
}
|
|
17
19
|
}
|
|
18
20
|
return newArr;
|
|
@@ -25,7 +27,11 @@ export function parseAllOf(schema, refs) {
|
|
|
25
27
|
const item = schema.allOf[0];
|
|
26
28
|
return parseSchema(item, {
|
|
27
29
|
...refs,
|
|
28
|
-
path: [
|
|
30
|
+
path: [
|
|
31
|
+
...refs.path,
|
|
32
|
+
"allOf",
|
|
33
|
+
item[originalIndexKey] ?? 0,
|
|
34
|
+
],
|
|
29
35
|
});
|
|
30
36
|
}
|
|
31
37
|
else {
|
|
@@ -14,8 +14,8 @@ export const parseIfThenElse = (schema, refs) => {
|
|
|
14
14
|
? ${$then}.safeParse(value)
|
|
15
15
|
: ${$else}.safeParse(value);
|
|
16
16
|
if (!result.success) {
|
|
17
|
-
const issues = result.error.issues
|
|
18
|
-
issues.forEach((issue) => ctx.addIssue(issue))
|
|
17
|
+
const issues = result.error.issues;
|
|
18
|
+
issues.forEach((issue) => ctx.addIssue({ ...issue }))
|
|
19
19
|
}
|
|
20
20
|
})`;
|
|
21
21
|
// Store original if/then/else for JSON Schema round-trip
|
|
@@ -197,6 +197,7 @@ export function parseObject(objectSchema, refs) {
|
|
|
197
197
|
output += `.and(${parseAnyOf({
|
|
198
198
|
...objectSchema,
|
|
199
199
|
anyOf: objectSchema.anyOf.map((x) => typeof x === "object" &&
|
|
200
|
+
x !== null &&
|
|
200
201
|
!x.type &&
|
|
201
202
|
(x.properties || x.additionalProperties || x.patternProperties)
|
|
202
203
|
? { ...x, type: "object" }
|
|
@@ -207,6 +208,7 @@ export function parseObject(objectSchema, refs) {
|
|
|
207
208
|
output += `.and(${parseOneOf({
|
|
208
209
|
...objectSchema,
|
|
209
210
|
oneOf: objectSchema.oneOf.map((x) => typeof x === "object" &&
|
|
211
|
+
x !== null &&
|
|
210
212
|
!x.type &&
|
|
211
213
|
(x.properties || x.additionalProperties || x.patternProperties)
|
|
212
214
|
? { ...x, type: "object" }
|
|
@@ -217,6 +219,7 @@ export function parseObject(objectSchema, refs) {
|
|
|
217
219
|
output += `.and(${parseAllOf({
|
|
218
220
|
...objectSchema,
|
|
219
221
|
allOf: objectSchema.allOf.map((x) => typeof x === "object" &&
|
|
222
|
+
x !== null &&
|
|
220
223
|
!x.type &&
|
|
221
224
|
(x.properties || x.additionalProperties || x.patternProperties)
|
|
222
225
|
? { ...x, type: "object" }
|
|
@@ -230,6 +233,7 @@ export function parseObject(objectSchema, refs) {
|
|
|
230
233
|
// propertyNames
|
|
231
234
|
if (objectSchema.propertyNames) {
|
|
232
235
|
const normalizedPropNames = typeof objectSchema.propertyNames === "object" &&
|
|
236
|
+
objectSchema.propertyNames !== null &&
|
|
233
237
|
!objectSchema.propertyNames.type &&
|
|
234
238
|
objectSchema.propertyNames.pattern
|
|
235
239
|
? { ...objectSchema.propertyNames, type: "string" }
|
|
@@ -258,12 +262,12 @@ export function parseObject(objectSchema, refs) {
|
|
|
258
262
|
if (entries.length) {
|
|
259
263
|
output += `.superRefine((obj, ctx) => {
|
|
260
264
|
${entries
|
|
261
|
-
.map(([key, schema]
|
|
265
|
+
.map(([key, schema]) => {
|
|
262
266
|
const parsed = parseSchema(schema, { ...refs, path: [...refs.path, "dependentSchemas", key] });
|
|
263
267
|
return `if (Object.prototype.hasOwnProperty.call(obj, ${JSON.stringify(key)})) {
|
|
264
268
|
const result = ${parsed}.safeParse(obj);
|
|
265
269
|
if (!result.success) {
|
|
266
|
-
ctx.addIssue({ code: "custom", message: "Dependent schema failed", path: [], params: { issues: result.error.issues } });
|
|
270
|
+
ctx.addIssue({ code: "custom", message: ${objectSchema.errorMessage?.dependentSchemas ?? JSON.stringify("Dependent schema failed")}, path: [], params: { issues: result.error.issues } });
|
|
267
271
|
}
|
|
268
272
|
}`;
|
|
269
273
|
})
|
|
@@ -275,7 +279,8 @@ export function parseObject(objectSchema, refs) {
|
|
|
275
279
|
if (objectSchema.dependentRequired && typeof objectSchema.dependentRequired === "object") {
|
|
276
280
|
const entries = Object.entries(objectSchema.dependentRequired);
|
|
277
281
|
if (entries.length) {
|
|
278
|
-
const depRequiredMessage = objectSchema.errorMessage?.dependentRequired ??
|
|
282
|
+
const depRequiredMessage = objectSchema.errorMessage?.dependentRequired ??
|
|
283
|
+
"Dependent required properties missing";
|
|
279
284
|
output += `.superRefine((obj, ctx) => {
|
|
280
285
|
${entries
|
|
281
286
|
.map(([prop, deps]) => {
|
|
@@ -303,5 +308,8 @@ const shouldUseGetter = (parsed, refs) => {
|
|
|
303
308
|
if (refs.currentSchemaName && parsed.includes(refs.currentSchemaName)) {
|
|
304
309
|
return true;
|
|
305
310
|
}
|
|
311
|
+
if (refs.cycleRefNames?.has(parsed)) {
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
306
314
|
return Boolean(refs.inProgress && refs.inProgress.has(parsed));
|
|
307
315
|
};
|
|
@@ -30,9 +30,7 @@ export const parseSchema = (schema, refs = { seen: new Map(), path: [] }, blockM
|
|
|
30
30
|
if (typeof schema !== "object")
|
|
31
31
|
return schema ? anyOrUnknown(refs) : "z.never()";
|
|
32
32
|
const parentBase = refs.currentBaseUri ?? refs.rootBaseUri ?? "root:///";
|
|
33
|
-
const baseUri = typeof schema.$id === "string"
|
|
34
|
-
? resolveUri(parentBase, schema.$id)
|
|
35
|
-
: parentBase;
|
|
33
|
+
const baseUri = typeof schema.$id === "string" ? resolveUri(parentBase, schema.$id) : parentBase;
|
|
36
34
|
const dynamicAnchors = Array.isArray(refs.dynamicAnchors) ? [...refs.dynamicAnchors] : [];
|
|
37
35
|
if (typeof schema.$dynamicAnchor === "string") {
|
|
38
36
|
dynamicAnchors.push({
|
|
@@ -81,6 +79,9 @@ export const parseSchema = (schema, refs = { seen: new Map(), path: [] }, blockM
|
|
|
81
79
|
};
|
|
82
80
|
const parseRef = (schema, refs) => {
|
|
83
81
|
const refValue = schema.$dynamicRef ?? schema.$ref;
|
|
82
|
+
if (typeof refValue !== "string") {
|
|
83
|
+
return anyOrUnknown(refs);
|
|
84
|
+
}
|
|
84
85
|
const resolved = resolveRef(schema, refValue, refs);
|
|
85
86
|
if (!resolved) {
|
|
86
87
|
refs.onUnresolvedRef?.(refValue, refs.path);
|
|
@@ -119,6 +120,14 @@ const parseRef = (schema, refs) => {
|
|
|
119
120
|
// (or is currently being resolved). This avoids TDZ on true cycles while
|
|
120
121
|
// letting ordered, acyclic refs stay direct.
|
|
121
122
|
if (isSameCycle || refs.inProgress.has(refName)) {
|
|
123
|
+
const inObjectProperty = refs.path.includes("properties") ||
|
|
124
|
+
refs.path.includes("patternProperties") ||
|
|
125
|
+
refs.path.includes("additionalProperties");
|
|
126
|
+
if (inObjectProperty && refName === refs.currentSchemaName) {
|
|
127
|
+
// Getter properties defer evaluation, so a direct reference avoids extra lazies
|
|
128
|
+
// for self-recursion.
|
|
129
|
+
return refName;
|
|
130
|
+
}
|
|
122
131
|
return `z.lazy(() => ${refName})`;
|
|
123
132
|
}
|
|
124
133
|
return refName;
|
|
@@ -190,7 +199,10 @@ const resolveRef = (schemaNode, ref, refs) => {
|
|
|
190
199
|
const loaded = refs.resolveExternalRef(extBase);
|
|
191
200
|
if (loaded) {
|
|
192
201
|
// If async resolver is used synchronously here, it will be ignored; keep simple sync for now
|
|
193
|
-
const
|
|
202
|
+
const maybePromise = loaded;
|
|
203
|
+
const schema = typeof maybePromise.then === "function"
|
|
204
|
+
? undefined
|
|
205
|
+
: loaded;
|
|
194
206
|
if (schema) {
|
|
195
207
|
const { registry } = buildRefRegistry(schema, extBase);
|
|
196
208
|
registry.forEach((entry, k) => refs.refRegistry?.set(k, entry));
|
|
@@ -338,10 +350,10 @@ const selectParser = (schema, refs) => {
|
|
|
338
350
|
return parseNumber(schema);
|
|
339
351
|
}
|
|
340
352
|
else if (its.a.primitive(schema, "boolean")) {
|
|
341
|
-
return parseBoolean(
|
|
353
|
+
return parseBoolean();
|
|
342
354
|
}
|
|
343
355
|
else if (its.a.primitive(schema, "null")) {
|
|
344
|
-
return parseNull(
|
|
356
|
+
return parseNull();
|
|
345
357
|
}
|
|
346
358
|
else if (its.a.conditional(schema)) {
|
|
347
359
|
return parseIfThenElse(schema, refs);
|
|
@@ -286,7 +286,7 @@ export const parseString = (schema, refs) => {
|
|
|
286
286
|
if (contentMediaType != "") {
|
|
287
287
|
r += contentMediaType;
|
|
288
288
|
r += withMessage(schema, "contentSchema", ({ value }) => {
|
|
289
|
-
if (value && value
|
|
289
|
+
if (value && typeof value === "object") {
|
|
290
290
|
return {
|
|
291
291
|
opener: `.pipe(${parseSchema(value, refContext)}`,
|
|
292
292
|
closer: ")",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveUri } from "./resolveUri.js";
|
|
2
|
-
export const buildRefRegistry = (schema, rootBaseUri = "root:///"
|
|
2
|
+
export const buildRefRegistry = (schema, rootBaseUri = "root:///") => {
|
|
3
3
|
const registry = new Map();
|
|
4
4
|
const walk = (node, baseUri, path) => {
|
|
5
5
|
if (typeof node !== "object" || node === null)
|