@gabrielbryk/json-schema-to-zod 2.7.4 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -1
- package/dist/cjs/core/analyzeSchema.js +62 -0
- package/dist/cjs/core/emitZod.js +141 -0
- package/dist/cjs/generators/generateBundle.js +355 -0
- package/dist/cjs/index.js +6 -0
- package/dist/cjs/jsonSchemaToZod.js +5 -73
- package/dist/cjs/parsers/parseArray.js +34 -15
- package/dist/cjs/parsers/parseIfThenElse.js +2 -1
- package/dist/cjs/parsers/parseNumber.js +81 -39
- package/dist/cjs/parsers/parseObject.js +24 -0
- package/dist/cjs/parsers/parseSchema.js +147 -25
- package/dist/cjs/parsers/parseString.js +294 -54
- package/dist/cjs/utils/buildRefRegistry.js +56 -0
- package/dist/cjs/utils/cycles.js +113 -0
- package/dist/cjs/utils/resolveUri.js +16 -0
- package/dist/cjs/utils/withMessage.js +4 -5
- package/dist/esm/core/analyzeSchema.js +58 -0
- package/dist/esm/core/emitZod.js +137 -0
- package/dist/esm/generators/generateBundle.js +351 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/jsonSchemaToZod.js +5 -73
- package/dist/esm/parsers/parseArray.js +34 -15
- package/dist/esm/parsers/parseIfThenElse.js +2 -1
- package/dist/esm/parsers/parseNumber.js +81 -39
- package/dist/esm/parsers/parseObject.js +24 -0
- package/dist/esm/parsers/parseSchema.js +147 -25
- package/dist/esm/parsers/parseString.js +294 -54
- package/dist/esm/utils/buildRefRegistry.js +52 -0
- package/dist/esm/utils/cycles.js +107 -0
- package/dist/esm/utils/resolveUri.js +12 -0
- package/dist/esm/utils/withMessage.js +4 -5
- package/dist/types/Types.d.ts +36 -0
- package/dist/types/core/analyzeSchema.d.ts +24 -0
- package/dist/types/core/emitZod.d.ts +2 -0
- package/dist/types/generators/generateBundle.d.ts +62 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/jsonSchemaToZod.d.ts +1 -1
- package/dist/types/parsers/parseSchema.d.ts +2 -1
- package/dist/types/parsers/parseString.d.ts +2 -2
- package/dist/types/utils/buildRefRegistry.d.ts +12 -0
- package/dist/types/utils/cycles.d.ts +7 -0
- package/dist/types/utils/jsdocs.d.ts +1 -1
- package/dist/types/utils/resolveUri.d.ts +1 -0
- package/dist/types/utils/withMessage.d.ts +6 -1
- package/docs/proposals/bundle-refactor.md +43 -0
- package/docs/proposals/ref-anchor-support.md +65 -0
- package/eslint.config.js +26 -0
- package/package.json +10 -4
- /package/{jest.config.js → jest.config.cjs} +0 -0
- /package/{postcjs.js → postcjs.cjs} +0 -0
- /package/{postesm.js → postesm.cjs} +0 -0
|
@@ -1,78 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.jsonSchemaToZod = void 0;
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const jsonSchemaToZod = (schema,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
const declarations = new Map();
|
|
11
|
-
const refNameByPointer = new Map();
|
|
12
|
-
const usedNames = new Set();
|
|
13
|
-
const exportRefs = rest.exportRefs ?? true;
|
|
14
|
-
const withMeta = rest.withMeta ?? true;
|
|
15
|
-
if (name)
|
|
16
|
-
usedNames.add(name);
|
|
17
|
-
const parsedSchema = (0, parseSchema_js_1.parseSchema)(schema, {
|
|
18
|
-
module,
|
|
19
|
-
name,
|
|
20
|
-
path: [],
|
|
21
|
-
seen: new Map(),
|
|
22
|
-
declarations,
|
|
23
|
-
inProgress: new Set(),
|
|
24
|
-
refNameByPointer,
|
|
25
|
-
usedNames,
|
|
26
|
-
root: schema,
|
|
27
|
-
currentSchemaName: name,
|
|
28
|
-
...rest,
|
|
29
|
-
withMeta,
|
|
30
|
-
});
|
|
31
|
-
const declarationBlock = declarations.size
|
|
32
|
-
? Array.from(declarations.entries())
|
|
33
|
-
.map(([refName, value]) => {
|
|
34
|
-
const shouldExport = exportRefs && module === "esm";
|
|
35
|
-
const decl = `${shouldExport ? "export " : ""}const ${refName} = ${value}`;
|
|
36
|
-
return decl;
|
|
37
|
-
})
|
|
38
|
-
.join("\n")
|
|
39
|
-
: "";
|
|
40
|
-
const jsdocs = rest.withJsdocs && typeof schema !== "boolean" && schema.description
|
|
41
|
-
? (0, jsdocs_js_1.expandJsdocs)(schema.description)
|
|
42
|
-
: "";
|
|
43
|
-
const lines = [];
|
|
44
|
-
if (module === "cjs" && !noImport) {
|
|
45
|
-
lines.push(`const { z } = require("zod")`);
|
|
46
|
-
}
|
|
47
|
-
if (module === "esm" && !noImport) {
|
|
48
|
-
lines.push(`import { z } from "zod"`);
|
|
49
|
-
}
|
|
50
|
-
if (declarationBlock) {
|
|
51
|
-
lines.push(declarationBlock);
|
|
52
|
-
}
|
|
53
|
-
if (module === "cjs") {
|
|
54
|
-
const payload = name ? `{ ${JSON.stringify(name)}: ${parsedSchema} }` : parsedSchema;
|
|
55
|
-
lines.push(`${jsdocs}module.exports = ${payload}`);
|
|
56
|
-
}
|
|
57
|
-
else if (module === "esm") {
|
|
58
|
-
lines.push(`${jsdocs}export ${name ? `const ${name} =` : `default`} ${parsedSchema}`);
|
|
59
|
-
}
|
|
60
|
-
else if (name) {
|
|
61
|
-
lines.push(`${jsdocs}const ${name} = ${parsedSchema}`);
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
lines.push(`${jsdocs}${parsedSchema}`);
|
|
65
|
-
}
|
|
66
|
-
let typeLine;
|
|
67
|
-
if (type && name) {
|
|
68
|
-
let typeName = typeof type === "string"
|
|
69
|
-
? type
|
|
70
|
-
: `${name[0].toUpperCase()}${name.substring(1)}`;
|
|
71
|
-
typeLine = `export type ${typeName} = z.infer<typeof ${name}>`;
|
|
72
|
-
}
|
|
73
|
-
const joined = lines.filter(Boolean).join("\n\n");
|
|
74
|
-
const combined = typeLine ? `${joined}\n${typeLine}` : joined;
|
|
75
|
-
const shouldEndWithNewline = module === "esm" || module === "cjs";
|
|
76
|
-
return `${combined}${shouldEndWithNewline ? "\n" : ""}`;
|
|
4
|
+
const analyzeSchema_js_1 = require("./core/analyzeSchema.js");
|
|
5
|
+
const emitZod_js_1 = require("./core/emitZod.js");
|
|
6
|
+
const jsonSchemaToZod = (schema, options = {}) => {
|
|
7
|
+
const analysis = (0, analyzeSchema_js_1.analyzeSchema)(schema, options);
|
|
8
|
+
return (0, emitZod_js_1.emitZod)(analysis);
|
|
77
9
|
};
|
|
78
10
|
exports.jsonSchemaToZod = jsonSchemaToZod;
|
|
@@ -32,22 +32,41 @@ const parseArray = (schema, refs) => {
|
|
|
32
32
|
...refs,
|
|
33
33
|
path: [...refs.path, "items"],
|
|
34
34
|
})})`;
|
|
35
|
-
r += (0, withMessage_js_1.withMessage)(schema, "minItems", ({ json }) =>
|
|
36
|
-
`.min(${json}`,
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
")",
|
|
44
|
-
|
|
35
|
+
r += (0, withMessage_js_1.withMessage)(schema, "minItems", ({ json }) => ({
|
|
36
|
+
opener: `.min(${json}`,
|
|
37
|
+
closer: ")",
|
|
38
|
+
messagePrefix: ", { error: ",
|
|
39
|
+
messageCloser: " })",
|
|
40
|
+
}));
|
|
41
|
+
r += (0, withMessage_js_1.withMessage)(schema, "maxItems", ({ json }) => ({
|
|
42
|
+
opener: `.max(${json}`,
|
|
43
|
+
closer: ")",
|
|
44
|
+
messagePrefix: ", { error: ",
|
|
45
|
+
messageCloser: " })",
|
|
46
|
+
}));
|
|
45
47
|
if (schema.uniqueItems === true) {
|
|
46
|
-
r += (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
r += `.superRefine((arr, ctx) => {
|
|
49
|
+
const seen = new Set();
|
|
50
|
+
for (const [index, value] of arr.entries()) {
|
|
51
|
+
let key;
|
|
52
|
+
if (value && typeof value === "object") {
|
|
53
|
+
try {
|
|
54
|
+
key = JSON.stringify(value);
|
|
55
|
+
} catch {
|
|
56
|
+
key = String(value);
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
key = JSON.stringify(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (seen.has(key)) {
|
|
63
|
+
ctx.addIssue({ code: "custom", message: "Array items must be unique", path: [index] });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
seen.add(key);
|
|
68
|
+
}
|
|
69
|
+
})`;
|
|
51
70
|
}
|
|
52
71
|
if (schema.contains) {
|
|
53
72
|
const containsSchema = (0, parseSchema_js_1.parseSchema)(schema.contains, {
|
|
@@ -17,7 +17,8 @@ const parseIfThenElse = (schema, refs) => {
|
|
|
17
17
|
? ${$then}.safeParse(value)
|
|
18
18
|
: ${$else}.safeParse(value);
|
|
19
19
|
if (!result.success) {
|
|
20
|
-
result.error.
|
|
20
|
+
const issues = result.error.issues ?? result.error.errors ?? [];
|
|
21
|
+
issues.forEach((issue) => ctx.addIssue(issue))
|
|
21
22
|
}
|
|
22
23
|
})`;
|
|
23
24
|
// Store original if/then/else for JSON Schema round-trip
|
|
@@ -3,71 +3,113 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.parseNumber = void 0;
|
|
4
4
|
const withMessage_js_1 = require("../utils/withMessage.js");
|
|
5
5
|
const parseNumber = (schema) => {
|
|
6
|
-
|
|
6
|
+
const formatError = schema.errorMessage?.format;
|
|
7
|
+
const numericFormatMap = {
|
|
8
|
+
int32: "z.int32",
|
|
9
|
+
uint32: "z.uint32",
|
|
10
|
+
float32: "z.float32",
|
|
11
|
+
float64: "z.float64",
|
|
12
|
+
safeint: "z.safeint",
|
|
13
|
+
int64: "z.int64",
|
|
14
|
+
uint64: "z.uint64",
|
|
15
|
+
};
|
|
16
|
+
const mappedFormat = schema.format && numericFormatMap[schema.format] ? numericFormatMap[schema.format] : undefined;
|
|
17
|
+
const formatParams = formatError !== undefined ? `{ error: ${JSON.stringify(formatError)} }` : "";
|
|
18
|
+
let r = mappedFormat ? `${mappedFormat}(${formatParams})` : "z.number()";
|
|
7
19
|
if (schema.type === "integer") {
|
|
8
|
-
|
|
20
|
+
if (!mappedFormat) {
|
|
21
|
+
r += (0, withMessage_js_1.withMessage)(schema, "type", () => ({
|
|
22
|
+
opener: ".int(",
|
|
23
|
+
closer: ")",
|
|
24
|
+
messagePrefix: "{ error: ",
|
|
25
|
+
messageCloser: " })",
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
9
28
|
}
|
|
10
29
|
else {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
30
|
+
if (!mappedFormat) {
|
|
31
|
+
r += (0, withMessage_js_1.withMessage)(schema, "format", ({ value }) => {
|
|
32
|
+
if (value === "int64") {
|
|
33
|
+
return {
|
|
34
|
+
opener: ".int(",
|
|
35
|
+
closer: ")",
|
|
36
|
+
messagePrefix: "{ error: ",
|
|
37
|
+
messageCloser: " })",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
16
42
|
}
|
|
17
43
|
r += (0, withMessage_js_1.withMessage)(schema, "multipleOf", ({ value, json }) => {
|
|
18
44
|
if (value === 1) {
|
|
19
45
|
if (r.startsWith("z.number().int(")) {
|
|
20
46
|
return;
|
|
21
47
|
}
|
|
22
|
-
return
|
|
48
|
+
return {
|
|
49
|
+
opener: ".int(",
|
|
50
|
+
closer: ")",
|
|
51
|
+
messagePrefix: "{ error: ",
|
|
52
|
+
messageCloser: " })",
|
|
53
|
+
};
|
|
23
54
|
}
|
|
24
|
-
return
|
|
55
|
+
return {
|
|
56
|
+
opener: `.multipleOf(${json}`,
|
|
57
|
+
closer: ")",
|
|
58
|
+
messagePrefix: ", { error: ",
|
|
59
|
+
messageCloser: " })",
|
|
60
|
+
};
|
|
25
61
|
});
|
|
26
62
|
if (typeof schema.minimum === "number") {
|
|
27
63
|
if (schema.exclusiveMinimum === true) {
|
|
28
|
-
r += (0, withMessage_js_1.withMessage)(schema, "minimum", ({ json }) =>
|
|
29
|
-
`.gt(${json}`,
|
|
30
|
-
|
|
31
|
-
"
|
|
32
|
-
|
|
64
|
+
r += (0, withMessage_js_1.withMessage)(schema, "minimum", ({ json }) => ({
|
|
65
|
+
opener: `.gt(${json}`,
|
|
66
|
+
closer: ")",
|
|
67
|
+
messagePrefix: ", { error: ",
|
|
68
|
+
messageCloser: " })",
|
|
69
|
+
}));
|
|
33
70
|
}
|
|
34
71
|
else {
|
|
35
|
-
r += (0, withMessage_js_1.withMessage)(schema, "minimum", ({ json }) =>
|
|
36
|
-
`.gte(${json}`,
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
|
|
72
|
+
r += (0, withMessage_js_1.withMessage)(schema, "minimum", ({ json }) => ({
|
|
73
|
+
opener: `.gte(${json}`,
|
|
74
|
+
closer: ")",
|
|
75
|
+
messagePrefix: ", { error: ",
|
|
76
|
+
messageCloser: " })",
|
|
77
|
+
}));
|
|
40
78
|
}
|
|
41
79
|
}
|
|
42
80
|
else if (typeof schema.exclusiveMinimum === "number") {
|
|
43
|
-
r += (0, withMessage_js_1.withMessage)(schema, "exclusiveMinimum", ({ json }) =>
|
|
44
|
-
`.gt(${json}`,
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
|
|
81
|
+
r += (0, withMessage_js_1.withMessage)(schema, "exclusiveMinimum", ({ json }) => ({
|
|
82
|
+
opener: `.gt(${json}`,
|
|
83
|
+
closer: ")",
|
|
84
|
+
messagePrefix: ", { error: ",
|
|
85
|
+
messageCloser: " })",
|
|
86
|
+
}));
|
|
48
87
|
}
|
|
49
88
|
if (typeof schema.maximum === "number") {
|
|
50
89
|
if (schema.exclusiveMaximum === true) {
|
|
51
|
-
r += (0, withMessage_js_1.withMessage)(schema, "maximum", ({ json }) =>
|
|
52
|
-
`.lt(${json}`,
|
|
53
|
-
|
|
54
|
-
"
|
|
55
|
-
|
|
90
|
+
r += (0, withMessage_js_1.withMessage)(schema, "maximum", ({ json }) => ({
|
|
91
|
+
opener: `.lt(${json}`,
|
|
92
|
+
closer: ")",
|
|
93
|
+
messagePrefix: ", { error: ",
|
|
94
|
+
messageCloser: " })",
|
|
95
|
+
}));
|
|
56
96
|
}
|
|
57
97
|
else {
|
|
58
|
-
r += (0, withMessage_js_1.withMessage)(schema, "maximum", ({ json }) =>
|
|
59
|
-
`.lte(${json}`,
|
|
60
|
-
|
|
61
|
-
"
|
|
62
|
-
|
|
98
|
+
r += (0, withMessage_js_1.withMessage)(schema, "maximum", ({ json }) => ({
|
|
99
|
+
opener: `.lte(${json}`,
|
|
100
|
+
closer: ")",
|
|
101
|
+
messagePrefix: ", { error: ",
|
|
102
|
+
messageCloser: " })",
|
|
103
|
+
}));
|
|
63
104
|
}
|
|
64
105
|
}
|
|
65
106
|
else if (typeof schema.exclusiveMaximum === "number") {
|
|
66
|
-
r += (0, withMessage_js_1.withMessage)(schema, "exclusiveMaximum", ({ json }) =>
|
|
67
|
-
`.lt(${json}`,
|
|
68
|
-
|
|
69
|
-
"
|
|
70
|
-
|
|
107
|
+
r += (0, withMessage_js_1.withMessage)(schema, "exclusiveMaximum", ({ json }) => ({
|
|
108
|
+
opener: `.lt(${json}`,
|
|
109
|
+
closer: ")",
|
|
110
|
+
messagePrefix: ", { error: ",
|
|
111
|
+
messageCloser: " })",
|
|
112
|
+
}));
|
|
71
113
|
}
|
|
72
114
|
return r;
|
|
73
115
|
};
|
|
@@ -271,6 +271,30 @@ function parseObject(objectSchema, refs) {
|
|
|
271
271
|
}`;
|
|
272
272
|
})
|
|
273
273
|
.join("\n ")}
|
|
274
|
+
})`;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// dependentRequired
|
|
278
|
+
if (objectSchema.dependentRequired && typeof objectSchema.dependentRequired === "object") {
|
|
279
|
+
const entries = Object.entries(objectSchema.dependentRequired);
|
|
280
|
+
if (entries.length) {
|
|
281
|
+
const depRequiredMessage = objectSchema.errorMessage?.dependentRequired ?? "Dependent required properties missing";
|
|
282
|
+
output += `.superRefine((obj, ctx) => {
|
|
283
|
+
${entries
|
|
284
|
+
.map(([prop, deps]) => {
|
|
285
|
+
const arr = Array.isArray(deps) ? deps : [];
|
|
286
|
+
if (!arr.length)
|
|
287
|
+
return "";
|
|
288
|
+
const jsonDeps = JSON.stringify(arr);
|
|
289
|
+
return `if (Object.prototype.hasOwnProperty.call(obj, ${JSON.stringify(prop)})) {
|
|
290
|
+
const missing = ${jsonDeps}.filter((d) => !Object.prototype.hasOwnProperty.call(obj, d));
|
|
291
|
+
if (missing.length) {
|
|
292
|
+
ctx.addIssue({ code: "custom", message: ${JSON.stringify(depRequiredMessage)}, path: [], params: { missing } });
|
|
293
|
+
}
|
|
294
|
+
}`;
|
|
295
|
+
})
|
|
296
|
+
.filter(Boolean)
|
|
297
|
+
.join("\n ")}
|
|
274
298
|
})`;
|
|
275
299
|
}
|
|
276
300
|
}
|
|
@@ -19,17 +19,33 @@ const parseOneOf_js_1 = require("./parseOneOf.js");
|
|
|
19
19
|
const parseSimpleDiscriminatedOneOf_js_1 = require("./parseSimpleDiscriminatedOneOf.js");
|
|
20
20
|
const parseNullable_js_1 = require("./parseNullable.js");
|
|
21
21
|
const anyOrUnknown_js_1 = require("../utils/anyOrUnknown.js");
|
|
22
|
+
const resolveUri_js_1 = require("../utils/resolveUri.js");
|
|
23
|
+
const buildRefRegistry_js_1 = require("../utils/buildRefRegistry.js");
|
|
22
24
|
const parseSchema = (schema, refs = { seen: new Map(), path: [] }, blockMeta) => {
|
|
23
25
|
// Ensure ref bookkeeping exists so $ref declarations and getter-based recursion work
|
|
24
26
|
refs.root = refs.root ?? schema;
|
|
27
|
+
refs.rootBaseUri = refs.rootBaseUri ?? "root:///";
|
|
25
28
|
refs.declarations = refs.declarations ?? new Map();
|
|
29
|
+
refs.dependencies = refs.dependencies ?? new Map();
|
|
26
30
|
refs.inProgress = refs.inProgress ?? new Set();
|
|
27
31
|
refs.refNameByPointer = refs.refNameByPointer ?? new Map();
|
|
28
32
|
refs.usedNames = refs.usedNames ?? new Set();
|
|
29
33
|
if (typeof schema !== "object")
|
|
30
34
|
return schema ? (0, anyOrUnknown_js_1.anyOrUnknown)(refs) : "z.never()";
|
|
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;
|
|
39
|
+
const dynamicAnchors = Array.isArray(refs.dynamicAnchors) ? [...refs.dynamicAnchors] : [];
|
|
40
|
+
if (typeof schema.$dynamicAnchor === "string") {
|
|
41
|
+
dynamicAnchors.push({
|
|
42
|
+
name: schema.$dynamicAnchor,
|
|
43
|
+
uri: baseUri,
|
|
44
|
+
path: refs.path,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
31
47
|
if (refs.parserOverride) {
|
|
32
|
-
const custom = refs.parserOverride(schema, refs);
|
|
48
|
+
const custom = refs.parserOverride(schema, { ...refs, currentBaseUri: baseUri, dynamicAnchors });
|
|
33
49
|
if (typeof custom === "string") {
|
|
34
50
|
return custom;
|
|
35
51
|
}
|
|
@@ -49,14 +65,14 @@ const parseSchema = (schema, refs = { seen: new Map(), path: [] }, blockMeta) =>
|
|
|
49
65
|
refs.seen.set(schema, seen);
|
|
50
66
|
}
|
|
51
67
|
if (exports.its.a.ref(schema)) {
|
|
52
|
-
const parsedRef = parseRef(schema, refs);
|
|
68
|
+
const parsedRef = parseRef(schema, { ...refs, currentBaseUri: baseUri, dynamicAnchors });
|
|
53
69
|
seen.r = parsedRef;
|
|
54
70
|
return parsedRef;
|
|
55
71
|
}
|
|
56
|
-
let parsed = selectParser(schema, refs);
|
|
72
|
+
let parsed = selectParser(schema, { ...refs, currentBaseUri: baseUri, dynamicAnchors });
|
|
57
73
|
if (!blockMeta) {
|
|
58
74
|
if (!refs.withoutDescribes) {
|
|
59
|
-
parsed = addDescribes(schema, parsed, refs);
|
|
75
|
+
parsed = addDescribes(schema, parsed, { ...refs, currentBaseUri: baseUri, dynamicAnchors });
|
|
60
76
|
}
|
|
61
77
|
if (!refs.withoutDefaults) {
|
|
62
78
|
parsed = addDefaults(schema, parsed);
|
|
@@ -68,23 +84,47 @@ const parseSchema = (schema, refs = { seen: new Map(), path: [] }, blockMeta) =>
|
|
|
68
84
|
};
|
|
69
85
|
exports.parseSchema = parseSchema;
|
|
70
86
|
const parseRef = (schema, refs) => {
|
|
71
|
-
const
|
|
87
|
+
const refValue = schema.$dynamicRef ?? schema.$ref;
|
|
88
|
+
const resolved = resolveRef(schema, refValue, refs);
|
|
72
89
|
if (!resolved) {
|
|
90
|
+
refs.onUnresolvedRef?.(refValue, refs.path);
|
|
73
91
|
return (0, anyOrUnknown_js_1.anyOrUnknown)(refs);
|
|
74
92
|
}
|
|
75
|
-
const { schema: target, path } = resolved;
|
|
76
|
-
const refName = getOrCreateRefName(
|
|
93
|
+
const { schema: target, path, pointerKey } = resolved;
|
|
94
|
+
const refName = getOrCreateRefName(pointerKey, path, refs);
|
|
77
95
|
if (!refs.declarations.has(refName) && !refs.inProgress.has(refName)) {
|
|
78
96
|
refs.inProgress.add(refName);
|
|
79
97
|
const declaration = (0, exports.parseSchema)(target, {
|
|
80
98
|
...refs,
|
|
81
99
|
path,
|
|
100
|
+
currentBaseUri: resolved.baseUri,
|
|
82
101
|
currentSchemaName: refName,
|
|
83
102
|
root: refs.root,
|
|
84
103
|
});
|
|
85
104
|
refs.inProgress.delete(refName);
|
|
86
105
|
refs.declarations.set(refName, declaration);
|
|
87
106
|
}
|
|
107
|
+
const current = refs.currentSchemaName;
|
|
108
|
+
if (current) {
|
|
109
|
+
const deps = refs.dependencies;
|
|
110
|
+
const set = deps.get(current) ?? new Set();
|
|
111
|
+
set.add(refName);
|
|
112
|
+
deps.set(current, set);
|
|
113
|
+
}
|
|
114
|
+
const currentComponent = refs.currentSchemaName
|
|
115
|
+
? refs.cycleComponentByName?.get(refs.currentSchemaName)
|
|
116
|
+
: undefined;
|
|
117
|
+
const targetComponent = refs.cycleComponentByName?.get(refName);
|
|
118
|
+
const isSameCycle = currentComponent !== undefined &&
|
|
119
|
+
targetComponent !== undefined &&
|
|
120
|
+
currentComponent === targetComponent &&
|
|
121
|
+
refs.cycleRefNames?.has(refName);
|
|
122
|
+
// Only lazy if the ref stays inside the current strongly-connected component
|
|
123
|
+
// (or is currently being resolved). This avoids TDZ on true cycles while
|
|
124
|
+
// letting ordered, acyclic refs stay direct.
|
|
125
|
+
if (isSameCycle || refs.inProgress.has(refName)) {
|
|
126
|
+
return `z.lazy(() => ${refName})`;
|
|
127
|
+
}
|
|
88
128
|
return refName;
|
|
89
129
|
};
|
|
90
130
|
const addDescribes = (schema, parsed, refs) => {
|
|
@@ -110,23 +150,93 @@ const addDescribes = (schema, parsed, refs) => {
|
|
|
110
150
|
}
|
|
111
151
|
return parsed;
|
|
112
152
|
};
|
|
113
|
-
const resolveRef = (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
153
|
+
const resolveRef = (schemaNode, ref, refs) => {
|
|
154
|
+
const base = refs.currentBaseUri ?? refs.rootBaseUri ?? "root:///";
|
|
155
|
+
// Handle dynamicRef lookup via dynamicAnchors stack
|
|
156
|
+
const isDynamic = typeof schemaNode.$dynamicRef === "string";
|
|
157
|
+
if (isDynamic && refs.dynamicAnchors && ref.startsWith("#")) {
|
|
158
|
+
const name = ref.slice(1);
|
|
159
|
+
for (let i = refs.dynamicAnchors.length - 1; i >= 0; i -= 1) {
|
|
160
|
+
const entry = refs.dynamicAnchors[i];
|
|
161
|
+
if (entry.name === name) {
|
|
162
|
+
const key = `${entry.uri}#${name}`;
|
|
163
|
+
const target = refs.refRegistry?.get(key);
|
|
164
|
+
if (target) {
|
|
165
|
+
return { schema: target.schema, path: target.path, baseUri: target.baseUri, pointerKey: key };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Resolve URI against base
|
|
171
|
+
const resolvedUri = (0, resolveUri_js_1.resolveUri)(base, ref);
|
|
172
|
+
const [uriBase, fragment] = resolvedUri.split("#");
|
|
173
|
+
const key = fragment ? `${uriBase}#${fragment}` : uriBase;
|
|
174
|
+
let regEntry = refs.refRegistry?.get(key);
|
|
175
|
+
if (regEntry) {
|
|
176
|
+
return { schema: regEntry.schema, path: regEntry.path, baseUri: regEntry.baseUri, pointerKey: key };
|
|
177
|
+
}
|
|
178
|
+
// Legacy recursive ref: treat as dynamic to __recursive__
|
|
179
|
+
if (schemaNode.$recursiveRef) {
|
|
180
|
+
const recursiveKey = `${base}#__recursive__`;
|
|
181
|
+
regEntry = refs.refRegistry?.get(recursiveKey);
|
|
182
|
+
if (regEntry) {
|
|
183
|
+
return {
|
|
184
|
+
schema: regEntry.schema,
|
|
185
|
+
path: regEntry.path,
|
|
186
|
+
baseUri: regEntry.baseUri,
|
|
187
|
+
pointerKey: recursiveKey,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// External resolver hook
|
|
192
|
+
const extBase = uriBaseFromRef(resolvedUri);
|
|
193
|
+
if (refs.resolveExternalRef && extBase && !isLocalBase(extBase, refs.rootBaseUri ?? "")) {
|
|
194
|
+
const loaded = refs.resolveExternalRef(extBase);
|
|
195
|
+
if (loaded) {
|
|
196
|
+
// If async resolver is used synchronously here, it will be ignored; keep simple sync for now
|
|
197
|
+
const schema = loaded.then ? undefined : loaded;
|
|
198
|
+
if (schema) {
|
|
199
|
+
const { registry } = (0, buildRefRegistry_js_1.buildRefRegistry)(schema, extBase);
|
|
200
|
+
registry.forEach((entry, k) => refs.refRegistry?.set(k, entry));
|
|
201
|
+
regEntry = refs.refRegistry?.get(key);
|
|
202
|
+
if (regEntry) {
|
|
203
|
+
return {
|
|
204
|
+
schema: regEntry.schema,
|
|
205
|
+
path: regEntry.path,
|
|
206
|
+
baseUri: regEntry.baseUri,
|
|
207
|
+
pointerKey: key,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Backward compatibility: JSON Pointer into root
|
|
214
|
+
if (refs.root && ref.startsWith("#/")) {
|
|
215
|
+
const rawSegments = ref
|
|
216
|
+
.slice(2)
|
|
217
|
+
.split("/")
|
|
218
|
+
.filter((segment) => segment.length > 0)
|
|
219
|
+
.map(decodePointerSegment);
|
|
220
|
+
let current = refs.root;
|
|
221
|
+
for (const segment of rawSegments) {
|
|
222
|
+
if (typeof current !== "object" || current === null)
|
|
223
|
+
return undefined;
|
|
224
|
+
current = current[segment];
|
|
225
|
+
}
|
|
226
|
+
return { schema: current, path: rawSegments, baseUri: base, pointerKey: ref };
|
|
227
|
+
}
|
|
228
|
+
return undefined;
|
|
128
229
|
};
|
|
129
230
|
const decodePointerSegment = (segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
231
|
+
const uriBaseFromRef = (resolvedUri) => {
|
|
232
|
+
const hashIdx = resolvedUri.indexOf("#");
|
|
233
|
+
return hashIdx === -1 ? resolvedUri : resolvedUri.slice(0, hashIdx);
|
|
234
|
+
};
|
|
235
|
+
const isLocalBase = (base, rootBase) => {
|
|
236
|
+
if (!rootBase)
|
|
237
|
+
return false;
|
|
238
|
+
return base === rootBase;
|
|
239
|
+
};
|
|
130
240
|
const getOrCreateRefName = (pointer, path, refs) => {
|
|
131
241
|
if (refs.refNameByPointer?.has(pointer)) {
|
|
132
242
|
return refs.refNameByPointer.get(pointer);
|
|
@@ -137,12 +247,24 @@ const getOrCreateRefName = (pointer, path, refs) => {
|
|
|
137
247
|
return preferred;
|
|
138
248
|
};
|
|
139
249
|
const buildNameFromPath = (path, used) => {
|
|
140
|
-
const filtered = path
|
|
250
|
+
const filtered = path
|
|
251
|
+
.map((segment, idx) => {
|
|
252
|
+
if (idx === 0 && (segment === "$defs" || segment === "definitions")) {
|
|
253
|
+
return undefined; // root-level defs prefix is redundant for naming
|
|
254
|
+
}
|
|
255
|
+
if (segment === "properties")
|
|
256
|
+
return undefined; // skip noisy properties segment
|
|
257
|
+
if (segment === "$defs" || segment === "definitions")
|
|
258
|
+
return "Defs";
|
|
259
|
+
return segment;
|
|
260
|
+
})
|
|
261
|
+
.filter((segment) => segment !== undefined);
|
|
141
262
|
const base = filtered.length
|
|
142
263
|
? filtered
|
|
143
264
|
.map((segment) => typeof segment === "number"
|
|
144
265
|
? `Ref${segment}`
|
|
145
266
|
: segment
|
|
267
|
+
.toString()
|
|
146
268
|
.replace(/[^a-zA-Z0-9_$]/g, " ")
|
|
147
269
|
.split(" ")
|
|
148
270
|
.filter(Boolean)
|
|
@@ -213,7 +335,7 @@ const selectParser = (schema, refs) => {
|
|
|
213
335
|
return (0, parseMultipleType_js_1.parseMultipleType)(schema, refs);
|
|
214
336
|
}
|
|
215
337
|
else if (exports.its.a.primitive(schema, "string")) {
|
|
216
|
-
return (0, parseString_js_1.parseString)(schema);
|
|
338
|
+
return (0, parseString_js_1.parseString)(schema, refs);
|
|
217
339
|
}
|
|
218
340
|
else if (exports.its.a.primitive(schema, "number") ||
|
|
219
341
|
exports.its.a.primitive(schema, "integer")) {
|
|
@@ -244,7 +366,7 @@ exports.its = {
|
|
|
244
366
|
nullable: (x) => x.nullable === true,
|
|
245
367
|
multipleType: (x) => Array.isArray(x.type),
|
|
246
368
|
not: (x) => x.not !== undefined,
|
|
247
|
-
ref: (x) => typeof x.$ref === "string",
|
|
369
|
+
ref: (x) => typeof x.$ref === "string" || typeof x.$dynamicRef === "string",
|
|
248
370
|
const: (x) => x.const !== undefined,
|
|
249
371
|
primitive: (x, p) => x.type === p,
|
|
250
372
|
conditional: (x) => Boolean("if" in x && x.if && "then" in x && "else" in x && x.then && x.else),
|