@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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# @gabrielbryk/json-schema-to-zod
|
|
2
2
|
|
|
3
|
+
## 2.10.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 1c318a0: Ensure bundled outputs remain type-safe by grouping strongly connected defs, emitting lazy getters for recursive object refs, and reordering bundle members to avoid duplicate exports.
|
|
8
|
+
- 87526f0: Preserve precise types for recursive schemas by emitting typed lazy wrappers instead of erasing types, and add a workflow regression type-check to guard against inference blowups.
|
|
9
|
+
|
|
3
10
|
## 2.9.0
|
|
4
11
|
|
|
5
12
|
### Minor Changes
|
|
@@ -11,6 +18,11 @@
|
|
|
11
18
|
### Patch Changes
|
|
12
19
|
|
|
13
20
|
- b8f6248: - Commit remaining utility, ref resolution test, and config updates.
|
|
21
|
+
- 04b6c6b: Fix bundle output by emitting z.lazy for cyclical refs outside object properties, use ZodError.issues in generated conditionals, and make nested type extraction array-aware to avoid invalid indexers.
|
|
22
|
+
- b8f7b29: Fix type errors in CI by replacing symbol-based allOf indexing, guarding invalid refs, and tightening string content schema parsing types.
|
|
23
|
+
- 691cc5b: - Added ESLint (recommended + no-require-imports) and cleaned all lint issues across src/tests; tightened types and removed unused vars.
|
|
24
|
+
- Ensured ESM-friendly test evals and ref/anchor resolver code without require usage.
|
|
25
|
+
- 4d127fe: Remove fallback to the non-existent ZodError.errors property in generated conditional schemas; rely on ZodError.issues to avoid TypeScript errors.
|
|
14
26
|
- 7d257dd: - Ensure dist/esm emits real ESM with NodeNext settings and type:module, and update tests to run under ESM by providing createRequire shims.
|
|
15
27
|
|
|
16
28
|
## 2.8.0
|
package/dist/cjs/core/emitZod.js
CHANGED
|
@@ -73,6 +73,16 @@ const emitZod = (analysis) => {
|
|
|
73
73
|
const { module, name, type, noImport, exportRefs, withMeta, ...rest } = options;
|
|
74
74
|
const declarations = new Map();
|
|
75
75
|
const dependencies = new Map();
|
|
76
|
+
const reserveName = (base) => {
|
|
77
|
+
let candidate = base;
|
|
78
|
+
let i = 1;
|
|
79
|
+
while (usedNames.has(candidate) || declarations.has(candidate)) {
|
|
80
|
+
candidate = `${base}${i}`;
|
|
81
|
+
i += 1;
|
|
82
|
+
}
|
|
83
|
+
usedNames.add(candidate);
|
|
84
|
+
return candidate;
|
|
85
|
+
};
|
|
76
86
|
const parsedSchema = (0, parseSchema_js_1.parseSchema)(schema, {
|
|
77
87
|
module,
|
|
78
88
|
name,
|
|
@@ -94,15 +104,21 @@ const emitZod = (analysis) => {
|
|
|
94
104
|
});
|
|
95
105
|
const declarationBlock = declarations.size
|
|
96
106
|
? orderDeclarations(Array.from(declarations.entries()), dependencies)
|
|
97
|
-
.
|
|
107
|
+
.flatMap(([refName, value]) => {
|
|
98
108
|
const shouldExport = exportRefs && module === "esm";
|
|
99
|
-
const
|
|
100
|
-
|
|
109
|
+
const isCycle = cycleRefNames.has(refName);
|
|
110
|
+
if (!isCycle) {
|
|
111
|
+
return [`${shouldExport ? "export " : ""}const ${refName} = ${value}`];
|
|
112
|
+
}
|
|
113
|
+
const baseName = `${refName}Def`;
|
|
114
|
+
const lines = [`const ${baseName} = ${value}`];
|
|
115
|
+
lines.push(`${shouldExport ? "export " : ""}const ${refName} = ${baseName}`);
|
|
116
|
+
return lines;
|
|
101
117
|
})
|
|
102
118
|
.join("\n")
|
|
103
119
|
: "";
|
|
104
|
-
const jsdocs = rest.withJsdocs && typeof schema
|
|
105
|
-
? (0, jsdocs_js_1.expandJsdocs)(schema.description)
|
|
120
|
+
const jsdocs = rest.withJsdocs && typeof schema === "object" && schema !== null && "description" in schema
|
|
121
|
+
? (0, jsdocs_js_1.expandJsdocs)(String(schema.description ?? ""))
|
|
106
122
|
: "";
|
|
107
123
|
const lines = [];
|
|
108
124
|
if (module === "cjs" && !noImport) {
|
|
@@ -11,23 +11,27 @@ const generateSchemaBundle = (schema, options = {}) => {
|
|
|
11
11
|
const defs = schema.$defs || {};
|
|
12
12
|
const definitions = schema.definitions || {};
|
|
13
13
|
const defNames = Object.keys(defs);
|
|
14
|
-
const { rootName, rootTypeName, defInfoMap } = buildBundleContext(defNames, defs, options);
|
|
14
|
+
const { rootName, rootTypeName, defInfoMap, groups } = buildBundleContext(defNames, defs, options);
|
|
15
15
|
const files = [];
|
|
16
|
-
const targets = planBundleTargets(schema, defs, definitions, defNames, options, rootName, rootTypeName);
|
|
16
|
+
const targets = planBundleTargets(schema, defs, definitions, defNames, options, rootName, defInfoMap, rootTypeName);
|
|
17
17
|
for (const target of targets) {
|
|
18
18
|
const usedRefs = target.usedRefs;
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
19
|
+
const zodParts = [];
|
|
20
|
+
for (const member of target.members) {
|
|
21
|
+
const analysis = (0, analyzeSchema_js_1.analyzeSchema)(member.schemaWithDefs, {
|
|
22
|
+
...options,
|
|
23
|
+
module,
|
|
24
|
+
name: member.schemaName,
|
|
25
|
+
type: member.typeName,
|
|
26
|
+
parserOverride: createRefHandler(member.defName, defInfoMap, usedRefs, {
|
|
27
|
+
...(member.schemaWithDefs.$defs || {}),
|
|
28
|
+
...(member.schemaWithDefs.definitions || {}),
|
|
29
|
+
}, options, target.groupId),
|
|
30
|
+
});
|
|
31
|
+
const zodSchema = (0, emitZod_js_1.emitZod)(analysis);
|
|
32
|
+
zodParts.push(zodSchema);
|
|
33
|
+
}
|
|
34
|
+
const finalSchema = buildSchemaFile(zodParts, usedRefs, defInfoMap, module, target);
|
|
31
35
|
files.push({ fileName: target.fileName, contents: finalSchema });
|
|
32
36
|
}
|
|
33
37
|
// Nested types extraction (optional)
|
|
@@ -57,13 +61,16 @@ const buildDefInfoMap = (defNames, defs, options) => {
|
|
|
57
61
|
const pascalName = toPascalCase(defName);
|
|
58
62
|
const schemaName = options.splitDefs?.schemaName?.(defName, { isRoot: false }) ?? `${pascalName}Schema`;
|
|
59
63
|
const typeName = options.splitDefs?.typeName?.(defName, { isRoot: false }) ?? pascalName;
|
|
64
|
+
const fileName = options.splitDefs?.fileName?.(defName, { isRoot: false }) ?? `${defName}.schema.ts`;
|
|
60
65
|
map.set(defName, {
|
|
61
66
|
name: defName,
|
|
62
67
|
pascalName,
|
|
63
68
|
schemaName,
|
|
64
69
|
typeName,
|
|
70
|
+
fileName,
|
|
65
71
|
dependencies,
|
|
66
72
|
hasCycle: false,
|
|
73
|
+
groupId: "",
|
|
67
74
|
});
|
|
68
75
|
}
|
|
69
76
|
return map;
|
|
@@ -76,13 +83,22 @@ const buildBundleContext = (defNames, defs, options) => {
|
|
|
76
83
|
if (info)
|
|
77
84
|
info.hasCycle = true;
|
|
78
85
|
}
|
|
86
|
+
const groups = buildSccGroups(defInfoMap);
|
|
87
|
+
for (const [groupId, members] of groups) {
|
|
88
|
+
for (const defName of members) {
|
|
89
|
+
const info = defInfoMap.get(defName);
|
|
90
|
+
if (info)
|
|
91
|
+
info.groupId = groupId;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
79
94
|
const rootName = options.splitDefs?.rootName ?? options.name ?? "RootSchema";
|
|
80
95
|
const rootTypeName = typeof options.type === "string"
|
|
81
96
|
? options.type
|
|
82
97
|
: options.splitDefs?.rootTypeName ?? (typeof options.type === "boolean" && options.type ? rootName : undefined);
|
|
83
|
-
return { defInfoMap, rootName, rootTypeName };
|
|
98
|
+
return { defInfoMap, rootName, rootTypeName, groups };
|
|
84
99
|
};
|
|
85
|
-
const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options) => {
|
|
100
|
+
const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options, currentGroupId) => {
|
|
101
|
+
const useLazyCrossRefs = options.refResolution?.lazyCrossRefs ?? true;
|
|
86
102
|
return (schema, refs) => {
|
|
87
103
|
if (typeof schema["$ref"] === "string") {
|
|
88
104
|
const refPath = schema["$ref"];
|
|
@@ -96,7 +112,7 @@ const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options
|
|
|
96
112
|
const refInfo = defInfoMap.get(refName);
|
|
97
113
|
if (refInfo) {
|
|
98
114
|
// Track imports when referencing other defs
|
|
99
|
-
if (refName !== currentDefName) {
|
|
115
|
+
if (refName !== currentDefName && refInfo.groupId !== currentGroupId) {
|
|
100
116
|
usedRefs.add(refName);
|
|
101
117
|
}
|
|
102
118
|
const isCycle = refName === currentDefName || (refInfo.hasCycle && !!currentDefName);
|
|
@@ -109,7 +125,14 @@ const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options
|
|
|
109
125
|
});
|
|
110
126
|
if (resolved)
|
|
111
127
|
return resolved;
|
|
112
|
-
if (isCycle &&
|
|
128
|
+
if (isCycle && useLazyCrossRefs) {
|
|
129
|
+
const inObjectProperty = refs.path.includes("properties") ||
|
|
130
|
+
refs.path.includes("patternProperties") ||
|
|
131
|
+
refs.path.includes("additionalProperties");
|
|
132
|
+
if (inObjectProperty && refName === currentDefName) {
|
|
133
|
+
// Self-recursion inside object getters can safely reference the schema name
|
|
134
|
+
return refInfo.schemaName;
|
|
135
|
+
}
|
|
113
136
|
return `z.lazy(() => ${refInfo.schemaName})`;
|
|
114
137
|
}
|
|
115
138
|
return refInfo.schemaName;
|
|
@@ -128,42 +151,73 @@ const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options
|
|
|
128
151
|
return undefined;
|
|
129
152
|
};
|
|
130
153
|
};
|
|
131
|
-
const buildSchemaFile = (
|
|
154
|
+
const buildSchemaFile = (zodCodeParts, usedRefs, defInfoMap, module, target) => {
|
|
132
155
|
if (module !== "esm")
|
|
133
|
-
return
|
|
134
|
-
const
|
|
156
|
+
return zodCodeParts.join("\n");
|
|
157
|
+
const groupFileById = new Map();
|
|
158
|
+
for (const info of defInfoMap.values()) {
|
|
159
|
+
if (!groupFileById.has(info.groupId)) {
|
|
160
|
+
groupFileById.set(info.groupId, info.fileName.replace(/\.ts$/, ".js"));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const importsByFile = new Map();
|
|
135
164
|
for (const refName of [...usedRefs].sort()) {
|
|
136
165
|
const refInfo = defInfoMap.get(refName);
|
|
137
166
|
if (refInfo) {
|
|
138
|
-
|
|
167
|
+
const groupFile = groupFileById.get(refInfo.groupId) ?? refInfo.fileName.replace(/\.ts$/, ".js");
|
|
168
|
+
const path = `./${groupFile}`;
|
|
169
|
+
const set = importsByFile.get(path) ?? new Set();
|
|
170
|
+
set.add(refInfo.schemaName);
|
|
171
|
+
importsByFile.set(path, set);
|
|
139
172
|
}
|
|
140
173
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
174
|
+
const imports = [];
|
|
175
|
+
for (const [path, names] of [...importsByFile.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
176
|
+
imports.push(`import { ${[...names].sort().join(", ")} } from '${path}';`);
|
|
177
|
+
}
|
|
178
|
+
const body = zodCodeParts
|
|
179
|
+
.map((code, idx) => {
|
|
180
|
+
if (idx === 0)
|
|
181
|
+
return code;
|
|
182
|
+
return code.replace(/^import \{ z \} from "zod"\n?/, "");
|
|
183
|
+
})
|
|
184
|
+
.join("\n");
|
|
185
|
+
return imports.length
|
|
186
|
+
? body.replace('import { z } from "zod"', `import { z } from "zod"\n${imports.join("\n")}`)
|
|
187
|
+
: body;
|
|
144
188
|
};
|
|
145
|
-
const planBundleTargets = (rootSchema, defs, definitions, defNames, options, rootName, rootTypeName) => {
|
|
189
|
+
const planBundleTargets = (rootSchema, defs, definitions, defNames, options, rootName, defInfoMap, rootTypeName) => {
|
|
146
190
|
const targets = [];
|
|
191
|
+
const groupById = new Map();
|
|
147
192
|
for (const defName of defNames) {
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
193
|
+
const info = defInfoMap.get(defName);
|
|
194
|
+
const gid = info?.groupId || defName;
|
|
195
|
+
if (!groupById.has(gid))
|
|
196
|
+
groupById.set(gid, []);
|
|
197
|
+
groupById.get(gid).push(defName);
|
|
198
|
+
}
|
|
199
|
+
for (const [groupId, memberDefs] of groupById.entries()) {
|
|
200
|
+
const orderedDefs = orderGroupMembers(memberDefs, defInfoMap);
|
|
201
|
+
const members = orderedDefs.map((defName) => {
|
|
202
|
+
const defSchema = defs[defName];
|
|
203
|
+
const defSchemaWithDefs = {
|
|
204
|
+
...defSchema,
|
|
205
|
+
$defs: { ...defs, ...defSchema?.$defs },
|
|
206
|
+
definitions: {
|
|
207
|
+
...defSchema.definitions,
|
|
208
|
+
...definitions,
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
const pascalName = toPascalCase(defName);
|
|
212
|
+
const schemaName = options.splitDefs?.schemaName?.(defName, { isRoot: false }) ?? `${pascalName}Schema`;
|
|
213
|
+
const typeName = options.splitDefs?.typeName?.(defName, { isRoot: false }) ?? pascalName;
|
|
214
|
+
return { defName, schemaWithDefs: defSchemaWithDefs, schemaName, typeName };
|
|
215
|
+
});
|
|
216
|
+
const fileName = defInfoMap.get(memberDefs[0])?.fileName ?? `${memberDefs[0]}.schema.ts`;
|
|
161
217
|
targets.push({
|
|
162
|
-
|
|
163
|
-
schemaWithDefs: defSchemaWithDefs,
|
|
164
|
-
schemaName,
|
|
165
|
-
typeName,
|
|
218
|
+
groupId,
|
|
166
219
|
fileName,
|
|
220
|
+
members,
|
|
167
221
|
usedRefs: new Set(),
|
|
168
222
|
isRoot: false,
|
|
169
223
|
});
|
|
@@ -171,17 +225,22 @@ const planBundleTargets = (rootSchema, defs, definitions, defNames, options, roo
|
|
|
171
225
|
if (options.splitDefs?.includeRoot ?? true) {
|
|
172
226
|
const rootFile = options.splitDefs?.fileName?.("root", { isRoot: true }) ?? "workflow.schema.ts";
|
|
173
227
|
targets.push({
|
|
174
|
-
|
|
175
|
-
schemaWithDefs: {
|
|
176
|
-
...rootSchema,
|
|
177
|
-
definitions: {
|
|
178
|
-
...rootSchema.definitions,
|
|
179
|
-
...definitions,
|
|
180
|
-
},
|
|
181
|
-
},
|
|
182
|
-
schemaName: rootName,
|
|
183
|
-
typeName: rootTypeName,
|
|
228
|
+
groupId: "root",
|
|
184
229
|
fileName: rootFile,
|
|
230
|
+
members: [
|
|
231
|
+
{
|
|
232
|
+
defName: null,
|
|
233
|
+
schemaWithDefs: {
|
|
234
|
+
...rootSchema,
|
|
235
|
+
definitions: {
|
|
236
|
+
...rootSchema.definitions,
|
|
237
|
+
...definitions,
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
schemaName: rootName,
|
|
241
|
+
typeName: rootTypeName,
|
|
242
|
+
},
|
|
243
|
+
],
|
|
185
244
|
usedRefs: new Set(),
|
|
186
245
|
isRoot: true,
|
|
187
246
|
});
|
|
@@ -212,6 +271,35 @@ const findRefDependencies = (schema, validDefNames) => {
|
|
|
212
271
|
traverse(schema);
|
|
213
272
|
return deps;
|
|
214
273
|
};
|
|
274
|
+
const orderGroupMembers = (defs, defInfoMap) => {
|
|
275
|
+
const inGroup = new Set(defs);
|
|
276
|
+
const visited = new Set();
|
|
277
|
+
const temp = new Set();
|
|
278
|
+
const result = [];
|
|
279
|
+
const visit = (name) => {
|
|
280
|
+
if (visited.has(name))
|
|
281
|
+
return;
|
|
282
|
+
if (temp.has(name)) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
temp.add(name);
|
|
286
|
+
const info = defInfoMap.get(name);
|
|
287
|
+
if (info) {
|
|
288
|
+
for (const dep of info.dependencies) {
|
|
289
|
+
if (inGroup.has(dep)) {
|
|
290
|
+
visit(dep);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
temp.delete(name);
|
|
295
|
+
visited.add(name);
|
|
296
|
+
result.push(name);
|
|
297
|
+
};
|
|
298
|
+
for (const name of defs) {
|
|
299
|
+
visit(name);
|
|
300
|
+
}
|
|
301
|
+
return result;
|
|
302
|
+
};
|
|
215
303
|
const detectCycles = (defInfoMap) => {
|
|
216
304
|
const cycleNodes = new Set();
|
|
217
305
|
const visited = new Set();
|
|
@@ -245,6 +333,52 @@ const detectCycles = (defInfoMap) => {
|
|
|
245
333
|
}
|
|
246
334
|
return cycleNodes;
|
|
247
335
|
};
|
|
336
|
+
const buildSccGroups = (defInfoMap) => {
|
|
337
|
+
const indexMap = new Map();
|
|
338
|
+
const lowLink = new Map();
|
|
339
|
+
const onStack = new Set();
|
|
340
|
+
const stack = [];
|
|
341
|
+
let index = 0;
|
|
342
|
+
const groups = new Map();
|
|
343
|
+
const strongConnect = (node) => {
|
|
344
|
+
indexMap.set(node, index);
|
|
345
|
+
lowLink.set(node, index);
|
|
346
|
+
index += 1;
|
|
347
|
+
stack.push(node);
|
|
348
|
+
onStack.add(node);
|
|
349
|
+
const info = defInfoMap.get(node);
|
|
350
|
+
if (info) {
|
|
351
|
+
for (const dep of info.dependencies) {
|
|
352
|
+
if (!indexMap.has(dep)) {
|
|
353
|
+
strongConnect(dep);
|
|
354
|
+
lowLink.set(node, Math.min(lowLink.get(node), lowLink.get(dep)));
|
|
355
|
+
}
|
|
356
|
+
else if (onStack.has(dep)) {
|
|
357
|
+
lowLink.set(node, Math.min(lowLink.get(node), indexMap.get(dep)));
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (lowLink.get(node) === indexMap.get(node)) {
|
|
362
|
+
const members = [];
|
|
363
|
+
let w;
|
|
364
|
+
do {
|
|
365
|
+
w = stack.pop();
|
|
366
|
+
if (w) {
|
|
367
|
+
onStack.delete(w);
|
|
368
|
+
members.push(w);
|
|
369
|
+
}
|
|
370
|
+
} while (w && w !== node);
|
|
371
|
+
const groupId = members.sort().join("__");
|
|
372
|
+
groups.set(groupId, members);
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
for (const name of defInfoMap.keys()) {
|
|
376
|
+
if (!indexMap.has(name)) {
|
|
377
|
+
strongConnect(name);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return groups;
|
|
381
|
+
};
|
|
248
382
|
const collectNestedTypes = (rootSchema, defs, defNames, rootTypeName) => {
|
|
249
383
|
const allNestedTypes = [];
|
|
250
384
|
for (const defName of defNames) {
|
|
@@ -289,7 +423,7 @@ const findNestedTypesInSchema = (schema, parentTypeName, defNames, currentPath =
|
|
|
289
423
|
}
|
|
290
424
|
// inline $defs
|
|
291
425
|
if (record.$defs && typeof record.$defs === "object") {
|
|
292
|
-
for (const [
|
|
426
|
+
for (const [, defSchema] of Object.entries(record.$defs)) {
|
|
293
427
|
nestedTypes.push(...findNestedTypesInSchema(defSchema, parentTypeName, defNames, currentPath));
|
|
294
428
|
}
|
|
295
429
|
}
|
|
@@ -320,6 +454,26 @@ const generateNestedTypesFile = (nestedTypes) => {
|
|
|
320
454
|
" * They are extracted using TypeScript indexed access types.",
|
|
321
455
|
" */",
|
|
322
456
|
"",
|
|
457
|
+
"type Access<T, P extends readonly (string | number)[]> =",
|
|
458
|
+
" P extends []",
|
|
459
|
+
" ? NonNullable<T>",
|
|
460
|
+
" : P extends readonly [infer H, ...infer R]",
|
|
461
|
+
" ? H extends \"items\"",
|
|
462
|
+
" ? Access<NonNullable<T> extends Array<infer U> ? U : unknown, Extract<R, (string | number)[]>>",
|
|
463
|
+
" : H extends \"additionalProperties\"",
|
|
464
|
+
" ? Access<NonNullable<T> extends Record<string, infer V> ? V : unknown, Extract<R, (string | number)[]>>",
|
|
465
|
+
" : H extends number",
|
|
466
|
+
" ? Access<NonNullable<T> extends Array<infer U> ? U : unknown, Extract<R, (string | number)[]>>",
|
|
467
|
+
" : H extends string",
|
|
468
|
+
" ? Access<",
|
|
469
|
+
" H extends keyof NonNullable<T>",
|
|
470
|
+
" ? NonNullable<NonNullable<T>[H]>",
|
|
471
|
+
" : unknown,",
|
|
472
|
+
" Extract<R, (string | number)[]>",
|
|
473
|
+
" >",
|
|
474
|
+
" : unknown",
|
|
475
|
+
" : unknown;",
|
|
476
|
+
"",
|
|
323
477
|
];
|
|
324
478
|
const byParent = new Map();
|
|
325
479
|
for (const info of nestedTypes) {
|
|
@@ -338,14 +492,15 @@ const generateNestedTypesFile = (nestedTypes) => {
|
|
|
338
492
|
lines.push(`import type { ${typeName} } from './${file}.schema.js';`);
|
|
339
493
|
}
|
|
340
494
|
lines.push("");
|
|
495
|
+
const buildAccessExpr = (parentType, propertyPath) => {
|
|
496
|
+
const path = propertyPath.map((prop) => (typeof prop === "number" ? prop : JSON.stringify(prop))).join(", ");
|
|
497
|
+
return `Access<${parentType}, [${path}]>`;
|
|
498
|
+
};
|
|
341
499
|
for (const [parentType, types] of [...byParent.entries()].sort()) {
|
|
342
500
|
lines.push(`// From ${parentType}`);
|
|
343
501
|
for (const info of types.sort((a, b) => a.typeName.localeCompare(b.typeName))) {
|
|
344
502
|
if (info.propertyPath.length > 0) {
|
|
345
|
-
|
|
346
|
-
for (const prop of info.propertyPath) {
|
|
347
|
-
accessExpr = `NonNullable<${accessExpr}['${prop}']>`;
|
|
348
|
-
}
|
|
503
|
+
const accessExpr = buildAccessExpr(parentType, info.propertyPath);
|
|
349
504
|
lines.push(`export type ${info.typeName} = ${accessExpr};`);
|
|
350
505
|
}
|
|
351
506
|
}
|
|
@@ -3,19 +3,21 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.parseAllOf = parseAllOf;
|
|
4
4
|
const parseSchema_js_1 = require("./parseSchema.js");
|
|
5
5
|
const half_js_1 = require("../utils/half.js");
|
|
6
|
-
const
|
|
6
|
+
const originalIndexKey = "__originalIndex";
|
|
7
7
|
const ensureOriginalIndex = (arr) => {
|
|
8
|
-
|
|
8
|
+
const newArr = [];
|
|
9
9
|
for (let i = 0; i < arr.length; i++) {
|
|
10
10
|
const item = arr[i];
|
|
11
11
|
if (typeof item === "boolean") {
|
|
12
|
-
newArr.push(item ? { [
|
|
12
|
+
newArr.push(item ? { [originalIndexKey]: i } : { [originalIndexKey]: i, not: {} });
|
|
13
13
|
}
|
|
14
|
-
else if (
|
|
14
|
+
else if (typeof item === "object" &&
|
|
15
|
+
item !== null &&
|
|
16
|
+
originalIndexKey in item) {
|
|
15
17
|
return arr;
|
|
16
18
|
}
|
|
17
19
|
else {
|
|
18
|
-
newArr.push({ ...item, [
|
|
20
|
+
newArr.push({ ...item, [originalIndexKey]: i });
|
|
19
21
|
}
|
|
20
22
|
}
|
|
21
23
|
return newArr;
|
|
@@ -28,7 +30,11 @@ function parseAllOf(schema, refs) {
|
|
|
28
30
|
const item = schema.allOf[0];
|
|
29
31
|
return (0, parseSchema_js_1.parseSchema)(item, {
|
|
30
32
|
...refs,
|
|
31
|
-
path: [
|
|
33
|
+
path: [
|
|
34
|
+
...refs.path,
|
|
35
|
+
"allOf",
|
|
36
|
+
item[originalIndexKey] ?? 0,
|
|
37
|
+
],
|
|
32
38
|
});
|
|
33
39
|
}
|
|
34
40
|
else {
|
|
@@ -17,8 +17,8 @@ const parseIfThenElse = (schema, refs) => {
|
|
|
17
17
|
? ${$then}.safeParse(value)
|
|
18
18
|
: ${$else}.safeParse(value);
|
|
19
19
|
if (!result.success) {
|
|
20
|
-
const issues = result.error.issues
|
|
21
|
-
issues.forEach((issue) => ctx.addIssue(issue))
|
|
20
|
+
const issues = result.error.issues;
|
|
21
|
+
issues.forEach((issue) => ctx.addIssue({ ...issue }))
|
|
22
22
|
}
|
|
23
23
|
})`;
|
|
24
24
|
// Store original if/then/else for JSON Schema round-trip
|
|
@@ -200,6 +200,7 @@ function parseObject(objectSchema, refs) {
|
|
|
200
200
|
output += `.and(${(0, parseAnyOf_js_1.parseAnyOf)({
|
|
201
201
|
...objectSchema,
|
|
202
202
|
anyOf: objectSchema.anyOf.map((x) => typeof x === "object" &&
|
|
203
|
+
x !== null &&
|
|
203
204
|
!x.type &&
|
|
204
205
|
(x.properties || x.additionalProperties || x.patternProperties)
|
|
205
206
|
? { ...x, type: "object" }
|
|
@@ -210,6 +211,7 @@ function parseObject(objectSchema, refs) {
|
|
|
210
211
|
output += `.and(${(0, parseOneOf_js_1.parseOneOf)({
|
|
211
212
|
...objectSchema,
|
|
212
213
|
oneOf: objectSchema.oneOf.map((x) => typeof x === "object" &&
|
|
214
|
+
x !== null &&
|
|
213
215
|
!x.type &&
|
|
214
216
|
(x.properties || x.additionalProperties || x.patternProperties)
|
|
215
217
|
? { ...x, type: "object" }
|
|
@@ -220,6 +222,7 @@ function parseObject(objectSchema, refs) {
|
|
|
220
222
|
output += `.and(${(0, parseAllOf_js_1.parseAllOf)({
|
|
221
223
|
...objectSchema,
|
|
222
224
|
allOf: objectSchema.allOf.map((x) => typeof x === "object" &&
|
|
225
|
+
x !== null &&
|
|
223
226
|
!x.type &&
|
|
224
227
|
(x.properties || x.additionalProperties || x.patternProperties)
|
|
225
228
|
? { ...x, type: "object" }
|
|
@@ -233,6 +236,7 @@ function parseObject(objectSchema, refs) {
|
|
|
233
236
|
// propertyNames
|
|
234
237
|
if (objectSchema.propertyNames) {
|
|
235
238
|
const normalizedPropNames = typeof objectSchema.propertyNames === "object" &&
|
|
239
|
+
objectSchema.propertyNames !== null &&
|
|
236
240
|
!objectSchema.propertyNames.type &&
|
|
237
241
|
objectSchema.propertyNames.pattern
|
|
238
242
|
? { ...objectSchema.propertyNames, type: "string" }
|
|
@@ -261,12 +265,12 @@ function parseObject(objectSchema, refs) {
|
|
|
261
265
|
if (entries.length) {
|
|
262
266
|
output += `.superRefine((obj, ctx) => {
|
|
263
267
|
${entries
|
|
264
|
-
.map(([key, schema]
|
|
268
|
+
.map(([key, schema]) => {
|
|
265
269
|
const parsed = (0, parseSchema_js_1.parseSchema)(schema, { ...refs, path: [...refs.path, "dependentSchemas", key] });
|
|
266
270
|
return `if (Object.prototype.hasOwnProperty.call(obj, ${JSON.stringify(key)})) {
|
|
267
271
|
const result = ${parsed}.safeParse(obj);
|
|
268
272
|
if (!result.success) {
|
|
269
|
-
ctx.addIssue({ code: "custom", message: "Dependent schema failed", path: [], params: { issues: result.error.issues } });
|
|
273
|
+
ctx.addIssue({ code: "custom", message: ${objectSchema.errorMessage?.dependentSchemas ?? JSON.stringify("Dependent schema failed")}, path: [], params: { issues: result.error.issues } });
|
|
270
274
|
}
|
|
271
275
|
}`;
|
|
272
276
|
})
|
|
@@ -278,7 +282,8 @@ function parseObject(objectSchema, refs) {
|
|
|
278
282
|
if (objectSchema.dependentRequired && typeof objectSchema.dependentRequired === "object") {
|
|
279
283
|
const entries = Object.entries(objectSchema.dependentRequired);
|
|
280
284
|
if (entries.length) {
|
|
281
|
-
const depRequiredMessage = objectSchema.errorMessage?.dependentRequired ??
|
|
285
|
+
const depRequiredMessage = objectSchema.errorMessage?.dependentRequired ??
|
|
286
|
+
"Dependent required properties missing";
|
|
282
287
|
output += `.superRefine((obj, ctx) => {
|
|
283
288
|
${entries
|
|
284
289
|
.map(([prop, deps]) => {
|
|
@@ -306,5 +311,8 @@ const shouldUseGetter = (parsed, refs) => {
|
|
|
306
311
|
if (refs.currentSchemaName && parsed.includes(refs.currentSchemaName)) {
|
|
307
312
|
return true;
|
|
308
313
|
}
|
|
314
|
+
if (refs.cycleRefNames?.has(parsed)) {
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
309
317
|
return Boolean(refs.inProgress && refs.inProgress.has(parsed));
|
|
310
318
|
};
|
|
@@ -33,9 +33,7 @@ const parseSchema = (schema, refs = { seen: new Map(), path: [] }, blockMeta) =>
|
|
|
33
33
|
if (typeof schema !== "object")
|
|
34
34
|
return schema ? (0, anyOrUnknown_js_1.anyOrUnknown)(refs) : "z.never()";
|
|
35
35
|
const parentBase = refs.currentBaseUri ?? refs.rootBaseUri ?? "root:///";
|
|
36
|
-
const baseUri = typeof schema.$id === "string"
|
|
37
|
-
? (0, resolveUri_js_1.resolveUri)(parentBase, schema.$id)
|
|
38
|
-
: parentBase;
|
|
36
|
+
const baseUri = typeof schema.$id === "string" ? (0, resolveUri_js_1.resolveUri)(parentBase, schema.$id) : parentBase;
|
|
39
37
|
const dynamicAnchors = Array.isArray(refs.dynamicAnchors) ? [...refs.dynamicAnchors] : [];
|
|
40
38
|
if (typeof schema.$dynamicAnchor === "string") {
|
|
41
39
|
dynamicAnchors.push({
|
|
@@ -85,6 +83,9 @@ const parseSchema = (schema, refs = { seen: new Map(), path: [] }, blockMeta) =>
|
|
|
85
83
|
exports.parseSchema = parseSchema;
|
|
86
84
|
const parseRef = (schema, refs) => {
|
|
87
85
|
const refValue = schema.$dynamicRef ?? schema.$ref;
|
|
86
|
+
if (typeof refValue !== "string") {
|
|
87
|
+
return (0, anyOrUnknown_js_1.anyOrUnknown)(refs);
|
|
88
|
+
}
|
|
88
89
|
const resolved = resolveRef(schema, refValue, refs);
|
|
89
90
|
if (!resolved) {
|
|
90
91
|
refs.onUnresolvedRef?.(refValue, refs.path);
|
|
@@ -123,6 +124,14 @@ const parseRef = (schema, refs) => {
|
|
|
123
124
|
// (or is currently being resolved). This avoids TDZ on true cycles while
|
|
124
125
|
// letting ordered, acyclic refs stay direct.
|
|
125
126
|
if (isSameCycle || refs.inProgress.has(refName)) {
|
|
127
|
+
const inObjectProperty = refs.path.includes("properties") ||
|
|
128
|
+
refs.path.includes("patternProperties") ||
|
|
129
|
+
refs.path.includes("additionalProperties");
|
|
130
|
+
if (inObjectProperty && refName === refs.currentSchemaName) {
|
|
131
|
+
// Getter properties defer evaluation, so a direct reference avoids extra lazies
|
|
132
|
+
// for self-recursion.
|
|
133
|
+
return refName;
|
|
134
|
+
}
|
|
126
135
|
return `z.lazy(() => ${refName})`;
|
|
127
136
|
}
|
|
128
137
|
return refName;
|
|
@@ -194,7 +203,10 @@ const resolveRef = (schemaNode, ref, refs) => {
|
|
|
194
203
|
const loaded = refs.resolveExternalRef(extBase);
|
|
195
204
|
if (loaded) {
|
|
196
205
|
// If async resolver is used synchronously here, it will be ignored; keep simple sync for now
|
|
197
|
-
const
|
|
206
|
+
const maybePromise = loaded;
|
|
207
|
+
const schema = typeof maybePromise.then === "function"
|
|
208
|
+
? undefined
|
|
209
|
+
: loaded;
|
|
198
210
|
if (schema) {
|
|
199
211
|
const { registry } = (0, buildRefRegistry_js_1.buildRefRegistry)(schema, extBase);
|
|
200
212
|
registry.forEach((entry, k) => refs.refRegistry?.set(k, entry));
|
|
@@ -342,10 +354,10 @@ const selectParser = (schema, refs) => {
|
|
|
342
354
|
return (0, parseNumber_js_1.parseNumber)(schema);
|
|
343
355
|
}
|
|
344
356
|
else if (exports.its.a.primitive(schema, "boolean")) {
|
|
345
|
-
return (0, parseBoolean_js_1.parseBoolean)(
|
|
357
|
+
return (0, parseBoolean_js_1.parseBoolean)();
|
|
346
358
|
}
|
|
347
359
|
else if (exports.its.a.primitive(schema, "null")) {
|
|
348
|
-
return (0, parseNull_js_1.parseNull)(
|
|
360
|
+
return (0, parseNull_js_1.parseNull)();
|
|
349
361
|
}
|
|
350
362
|
else if (exports.its.a.conditional(schema)) {
|
|
351
363
|
return (0, parseIfThenElse_js_1.parseIfThenElse)(schema, refs);
|
|
@@ -289,7 +289,7 @@ const parseString = (schema, refs) => {
|
|
|
289
289
|
if (contentMediaType != "") {
|
|
290
290
|
r += contentMediaType;
|
|
291
291
|
r += (0, withMessage_js_1.withMessage)(schema, "contentSchema", ({ value }) => {
|
|
292
|
-
if (value && value
|
|
292
|
+
if (value && typeof value === "object") {
|
|
293
293
|
return {
|
|
294
294
|
opener: `.pipe(${(0, parseSchema_js_1.parseSchema)(value, refContext)}`,
|
|
295
295
|
closer: ")",
|