@gabrielbryk/json-schema-to-zod 2.8.0 → 2.10.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 +18 -0
- package/dist/cjs/core/analyzeSchema.js +62 -0
- package/dist/cjs/core/emitZod.js +141 -0
- package/dist/cjs/generators/generateBundle.js +117 -63
- package/dist/cjs/index.js +4 -0
- package/dist/cjs/jsonSchemaToZod.js +5 -167
- package/dist/cjs/parsers/parseAllOf.js +12 -6
- package/dist/cjs/parsers/parseBoolean.js +1 -3
- package/dist/cjs/parsers/parseIfThenElse.js +1 -1
- package/dist/cjs/parsers/parseNull.js +1 -3
- package/dist/cjs/parsers/parseObject.js +8 -3
- package/dist/cjs/parsers/parseSchema.js +130 -26
- package/dist/cjs/parsers/parseString.js +1 -1
- package/dist/cjs/utils/buildRefRegistry.js +56 -0
- package/dist/cjs/utils/omit.js +3 -2
- package/dist/cjs/utils/resolveUri.js +16 -0
- package/dist/esm/Types.js +1 -2
- package/dist/esm/cli.js +10 -12
- package/dist/esm/core/analyzeSchema.js +58 -0
- package/dist/esm/core/emitZod.js +137 -0
- package/dist/esm/generators/generateBundle.js +118 -68
- package/dist/esm/index.js +34 -46
- package/dist/esm/jsonSchemaToZod.js +5 -171
- package/dist/esm/parsers/parseAllOf.js +17 -14
- package/dist/esm/parsers/parseAnyOf.js +6 -10
- package/dist/esm/parsers/parseArray.js +11 -15
- package/dist/esm/parsers/parseBoolean.js +1 -7
- package/dist/esm/parsers/parseConst.js +1 -5
- package/dist/esm/parsers/parseDefault.js +3 -7
- package/dist/esm/parsers/parseEnum.js +1 -5
- package/dist/esm/parsers/parseIfThenElse.js +6 -10
- package/dist/esm/parsers/parseMultipleType.js +3 -7
- package/dist/esm/parsers/parseNot.js +4 -8
- package/dist/esm/parsers/parseNull.js +1 -7
- package/dist/esm/parsers/parseNullable.js +4 -8
- package/dist/esm/parsers/parseNumber.js +11 -15
- package/dist/esm/parsers/parseObject.js +33 -31
- package/dist/esm/parsers/parseOneOf.js +6 -10
- package/dist/esm/parsers/parseSchema.js +187 -87
- package/dist/esm/parsers/parseSimpleDiscriminatedOneOf.js +6 -10
- package/dist/esm/parsers/parseString.js +12 -16
- package/dist/esm/utils/anyOrUnknown.js +1 -5
- package/dist/esm/utils/buildRefRegistry.js +52 -0
- package/dist/esm/utils/cliTools.js +7 -13
- package/dist/esm/utils/cycles.js +3 -9
- package/dist/esm/utils/half.js +1 -5
- package/dist/esm/utils/jsdocs.js +3 -8
- package/dist/esm/utils/omit.js +4 -7
- package/dist/esm/utils/resolveUri.js +12 -0
- package/dist/esm/utils/withMessage.js +1 -4
- package/dist/esm/zodToJsonSchema.js +1 -4
- package/dist/types/Types.d.ts +34 -3
- 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 +5 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/jsonSchemaToZod.d.ts +1 -1
- package/dist/types/parsers/parseBoolean.d.ts +1 -4
- package/dist/types/parsers/parseNull.d.ts +1 -4
- package/dist/types/parsers/parseSchema.d.ts +2 -1
- package/dist/types/utils/buildRefRegistry.d.ts +12 -0
- package/dist/types/utils/resolveUri.d.ts +1 -0
- 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,9 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const withMessage_js_1 = require("../utils/withMessage.js");
|
|
5
|
-
const parseSchema_js_1 = require("./parseSchema.js");
|
|
6
|
-
const parseString = (schema, refs) => {
|
|
1
|
+
import { withMessage } from "../utils/withMessage.js";
|
|
2
|
+
import { parseSchema } from "./parseSchema.js";
|
|
3
|
+
export const parseString = (schema, refs) => {
|
|
7
4
|
const formatError = schema.errorMessage?.format;
|
|
8
5
|
const refContext = ensureRefs(refs);
|
|
9
6
|
const topLevelFormatMap = {
|
|
@@ -29,7 +26,7 @@ const parseString = (schema, refs) => {
|
|
|
29
26
|
const formatHandled = Boolean(formatFn);
|
|
30
27
|
let formatWasHandled = formatHandled;
|
|
31
28
|
if (!formatHandled) {
|
|
32
|
-
r +=
|
|
29
|
+
r += withMessage(schema, "format", ({ value }) => {
|
|
33
30
|
switch (value) {
|
|
34
31
|
case "email":
|
|
35
32
|
formatWasHandled = true;
|
|
@@ -248,25 +245,25 @@ const parseString = (schema, refs) => {
|
|
|
248
245
|
if (schema.format && !formatWasHandled) {
|
|
249
246
|
refContext.onUnknownFormat?.(schema.format, refContext.path);
|
|
250
247
|
}
|
|
251
|
-
r +=
|
|
248
|
+
r += withMessage(schema, "pattern", ({ json }) => ({
|
|
252
249
|
opener: `.regex(new RegExp(${json})`,
|
|
253
250
|
closer: ")",
|
|
254
251
|
messagePrefix: ", { error: ",
|
|
255
252
|
messageCloser: " })",
|
|
256
253
|
}));
|
|
257
|
-
r +=
|
|
254
|
+
r += withMessage(schema, "minLength", ({ json }) => ({
|
|
258
255
|
opener: `.min(${json}`,
|
|
259
256
|
closer: ")",
|
|
260
257
|
messagePrefix: ", { error: ",
|
|
261
258
|
messageCloser: " })",
|
|
262
259
|
}));
|
|
263
|
-
r +=
|
|
260
|
+
r += withMessage(schema, "maxLength", ({ json }) => ({
|
|
264
261
|
opener: `.max(${json}`,
|
|
265
262
|
closer: ")",
|
|
266
263
|
messagePrefix: ", { error: ",
|
|
267
264
|
messageCloser: " })",
|
|
268
265
|
}));
|
|
269
|
-
r +=
|
|
266
|
+
r += withMessage(schema, "contentEncoding", ({ value }) => {
|
|
270
267
|
if (value === "base64") {
|
|
271
268
|
return {
|
|
272
269
|
opener: ".base64(",
|
|
@@ -276,7 +273,7 @@ const parseString = (schema, refs) => {
|
|
|
276
273
|
};
|
|
277
274
|
}
|
|
278
275
|
});
|
|
279
|
-
const contentMediaType =
|
|
276
|
+
const contentMediaType = withMessage(schema, "contentMediaType", ({ value }) => {
|
|
280
277
|
if (value === "application/json") {
|
|
281
278
|
return {
|
|
282
279
|
opener: '.transform((str, ctx) => { try { return JSON.parse(str); } catch (err) { ctx.addIssue({ code: "custom", message: "Invalid JSON" }); }}',
|
|
@@ -288,10 +285,10 @@ const parseString = (schema, refs) => {
|
|
|
288
285
|
});
|
|
289
286
|
if (contentMediaType != "") {
|
|
290
287
|
r += contentMediaType;
|
|
291
|
-
r +=
|
|
292
|
-
if (value && value
|
|
288
|
+
r += withMessage(schema, "contentSchema", ({ value }) => {
|
|
289
|
+
if (value && typeof value === "object") {
|
|
293
290
|
return {
|
|
294
|
-
opener: `.pipe(${
|
|
291
|
+
opener: `.pipe(${parseSchema(value, refContext)}`,
|
|
295
292
|
closer: ")",
|
|
296
293
|
messagePrefix: ", { error: ",
|
|
297
294
|
messageCloser: " })",
|
|
@@ -301,7 +298,6 @@ const parseString = (schema, refs) => {
|
|
|
301
298
|
}
|
|
302
299
|
return r;
|
|
303
300
|
};
|
|
304
|
-
exports.parseString = parseString;
|
|
305
301
|
function ensureRefs(refs) {
|
|
306
302
|
if (refs)
|
|
307
303
|
return refs;
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.anyOrUnknown = void 0;
|
|
4
1
|
/**
|
|
5
2
|
* Returns "z.unknown()" if the useUnknown option is enabled, otherwise "z.any()".
|
|
6
3
|
* This helper is used throughout the library for fallback cases.
|
|
@@ -8,7 +5,6 @@ exports.anyOrUnknown = void 0;
|
|
|
8
5
|
* @param refs - The refs object containing options
|
|
9
6
|
* @returns The appropriate Zod schema string
|
|
10
7
|
*/
|
|
11
|
-
const anyOrUnknown = (refs) => {
|
|
8
|
+
export const anyOrUnknown = (refs) => {
|
|
12
9
|
return refs?.useUnknown ? "z.unknown()" : "z.any()";
|
|
13
10
|
};
|
|
14
|
-
exports.anyOrUnknown = anyOrUnknown;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { resolveUri } from "./resolveUri.js";
|
|
2
|
+
export const buildRefRegistry = (schema, rootBaseUri = "root:///") => {
|
|
3
|
+
const registry = new Map();
|
|
4
|
+
const walk = (node, baseUri, path) => {
|
|
5
|
+
if (typeof node !== "object" || node === null)
|
|
6
|
+
return;
|
|
7
|
+
const obj = node;
|
|
8
|
+
const nextBase = obj.$id ? resolveUri(baseUri, obj.$id) : baseUri;
|
|
9
|
+
// Legacy recursive anchor
|
|
10
|
+
if (obj.$recursiveAnchor === true) {
|
|
11
|
+
const name = "__recursive__";
|
|
12
|
+
registry.set(`${nextBase}#${name}`, {
|
|
13
|
+
schema: node,
|
|
14
|
+
path,
|
|
15
|
+
baseUri: nextBase,
|
|
16
|
+
dynamic: true,
|
|
17
|
+
anchor: name,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
// Register base entry
|
|
21
|
+
registry.set(nextBase, { schema: node, path, baseUri: nextBase });
|
|
22
|
+
if (typeof obj.$anchor === "string") {
|
|
23
|
+
registry.set(`${nextBase}#${obj.$anchor}`, {
|
|
24
|
+
schema: node,
|
|
25
|
+
path,
|
|
26
|
+
baseUri: nextBase,
|
|
27
|
+
anchor: obj.$anchor,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
if (typeof obj.$dynamicAnchor === "string") {
|
|
31
|
+
const name = obj.$dynamicAnchor;
|
|
32
|
+
registry.set(`${nextBase}#${name}`, {
|
|
33
|
+
schema: node,
|
|
34
|
+
path,
|
|
35
|
+
baseUri: nextBase,
|
|
36
|
+
dynamic: true,
|
|
37
|
+
anchor: name,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
for (const key in obj) {
|
|
41
|
+
const value = obj[key];
|
|
42
|
+
if (Array.isArray(value)) {
|
|
43
|
+
value.forEach((v, i) => walk(v, nextBase, [...path, key, i]));
|
|
44
|
+
}
|
|
45
|
+
else if (typeof value === "object" && value !== null) {
|
|
46
|
+
walk(value, nextBase, [...path, key]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
walk(schema, rootBaseUri, []);
|
|
51
|
+
return { registry, rootBaseUri };
|
|
52
|
+
};
|
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
exports.parseArgs = parseArgs;
|
|
4
|
-
exports.parseOrReadJSON = parseOrReadJSON;
|
|
5
|
-
exports.readPipe = readPipe;
|
|
6
|
-
exports.printParams = printParams;
|
|
7
|
-
const fs_1 = require("fs");
|
|
8
|
-
function parseArgs(params, args, help) {
|
|
1
|
+
import { statSync, readFileSync } from "fs";
|
|
2
|
+
export function parseArgs(params, args, help) {
|
|
9
3
|
const result = {};
|
|
10
4
|
if (help) {
|
|
11
5
|
let index = args.indexOf("--help");
|
|
@@ -62,15 +56,15 @@ function parseArgs(params, args, help) {
|
|
|
62
56
|
}
|
|
63
57
|
return result;
|
|
64
58
|
}
|
|
65
|
-
function parseOrReadJSON(jsonOrPath) {
|
|
59
|
+
export function parseOrReadJSON(jsonOrPath) {
|
|
66
60
|
jsonOrPath = jsonOrPath.trim();
|
|
67
61
|
if (jsonOrPath.length < 255 &&
|
|
68
|
-
|
|
69
|
-
jsonOrPath =
|
|
62
|
+
statSync(jsonOrPath, { throwIfNoEntry: false })?.isFile()) {
|
|
63
|
+
jsonOrPath = readFileSync(jsonOrPath, "utf-8");
|
|
70
64
|
}
|
|
71
65
|
return JSON.parse(jsonOrPath);
|
|
72
66
|
}
|
|
73
|
-
function readPipe() {
|
|
67
|
+
export function readPipe() {
|
|
74
68
|
return new Promise((resolve, reject) => {
|
|
75
69
|
let buf = "";
|
|
76
70
|
process.stdin
|
|
@@ -85,7 +79,7 @@ function readPipe() {
|
|
|
85
79
|
});
|
|
86
80
|
});
|
|
87
81
|
}
|
|
88
|
-
function printParams(params) {
|
|
82
|
+
export function printParams(params) {
|
|
89
83
|
const longest = Object.keys(params).reduce((l, c) => (c.length > l ? c.length : l), 5);
|
|
90
84
|
const header = "Name " + " ".repeat(longest - 2) + "Short Description";
|
|
91
85
|
console.log(header);
|
package/dist/esm/utils/cycles.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.computeScc = exports.detectCycles = exports.findRefDependencies = void 0;
|
|
4
|
-
const findRefDependencies = (schema, validDefNames) => {
|
|
1
|
+
export const findRefDependencies = (schema, validDefNames) => {
|
|
5
2
|
const deps = new Set();
|
|
6
3
|
function traverse(obj) {
|
|
7
4
|
if (obj === null || typeof obj !== "object")
|
|
@@ -25,8 +22,7 @@ const findRefDependencies = (schema, validDefNames) => {
|
|
|
25
22
|
traverse(schema);
|
|
26
23
|
return deps;
|
|
27
24
|
};
|
|
28
|
-
|
|
29
|
-
const detectCycles = (defNames, deps) => {
|
|
25
|
+
export const detectCycles = (defNames, deps) => {
|
|
30
26
|
const cycleNodes = new Set();
|
|
31
27
|
const visited = new Set();
|
|
32
28
|
const recursionStack = new Set();
|
|
@@ -57,8 +53,7 @@ const detectCycles = (defNames, deps) => {
|
|
|
57
53
|
}
|
|
58
54
|
return cycleNodes;
|
|
59
55
|
};
|
|
60
|
-
|
|
61
|
-
const computeScc = (defNames, deps) => {
|
|
56
|
+
export const computeScc = (defNames, deps) => {
|
|
62
57
|
// Tarjan's algorithm, keeps mapping for quick "is this ref in my cycle?"
|
|
63
58
|
const index = new Map();
|
|
64
59
|
const lowlink = new Map();
|
|
@@ -110,4 +105,3 @@ const computeScc = (defNames, deps) => {
|
|
|
110
105
|
}
|
|
111
106
|
return { cycleMembers, componentByName };
|
|
112
107
|
};
|
|
113
|
-
exports.computeScc = computeScc;
|
package/dist/esm/utils/half.js
CHANGED
package/dist/esm/utils/jsdocs.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.addJsdocs = exports.expandJsdocs = void 0;
|
|
4
|
-
const expandJsdocs = (jsdocs) => {
|
|
1
|
+
export const expandJsdocs = (jsdocs) => {
|
|
5
2
|
const lines = jsdocs.split("\n");
|
|
6
3
|
const result = lines.length === 1
|
|
7
4
|
? lines[0]
|
|
@@ -9,12 +6,10 @@ const expandJsdocs = (jsdocs) => {
|
|
|
9
6
|
.join("\n")}\n`;
|
|
10
7
|
return `/**${result}*/\n`;
|
|
11
8
|
};
|
|
12
|
-
|
|
13
|
-
const addJsdocs = (schema, parsed) => {
|
|
9
|
+
export const addJsdocs = (schema, parsed) => {
|
|
14
10
|
const description = schema.description;
|
|
15
11
|
if (!description) {
|
|
16
12
|
return parsed;
|
|
17
13
|
}
|
|
18
|
-
return `\n${
|
|
14
|
+
return `\n${expandJsdocs(description)}${parsed}`;
|
|
19
15
|
};
|
|
20
|
-
exports.addJsdocs = addJsdocs;
|
package/dist/esm/utils/omit.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
if (!keys.includes(key)) {
|
|
6
|
-
acc[key] = obj[key];
|
|
1
|
+
export const omit = (obj, ...keys) => Object.keys(obj).reduce((acc, key) => {
|
|
2
|
+
const typedKey = key;
|
|
3
|
+
if (!keys.includes(typedKey)) {
|
|
4
|
+
acc[typedKey] = obj[typedKey];
|
|
7
5
|
}
|
|
8
6
|
return acc;
|
|
9
7
|
}, {});
|
|
10
|
-
exports.omit = omit;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const resolveUri = (base, ref) => {
|
|
2
|
+
try {
|
|
3
|
+
// If ref is absolute, new URL will accept it; otherwise resolves against base
|
|
4
|
+
return new URL(ref, base).toString();
|
|
5
|
+
}
|
|
6
|
+
catch {
|
|
7
|
+
// Fallback: simple concatenation to avoid throwing; keep ref as-is
|
|
8
|
+
if (ref.startsWith("#"))
|
|
9
|
+
return `${base}${ref}`;
|
|
10
|
+
return ref;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.withMessage = withMessage;
|
|
4
|
-
function withMessage(schema, key, get) {
|
|
1
|
+
export function withMessage(schema, key, get) {
|
|
5
2
|
const value = schema[key];
|
|
6
3
|
let r = "";
|
|
7
4
|
if (value !== undefined) {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* Post-processor for Zod's z.toJSONSchema() output.
|
|
4
3
|
*
|
|
@@ -15,8 +14,6 @@
|
|
|
15
14
|
* - patternProperties (stored in __jsonSchema.patternProperties)
|
|
16
15
|
* - if/then/else conditionals (stored in __jsonSchema.conditional)
|
|
17
16
|
*/
|
|
18
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
-
exports.reconstructJsonSchema = reconstructJsonSchema;
|
|
20
17
|
/**
|
|
21
18
|
* Recursively process a JSON Schema to reconstruct original features from __jsonSchema meta.
|
|
22
19
|
*
|
|
@@ -24,7 +21,7 @@ exports.reconstructJsonSchema = reconstructJsonSchema;
|
|
|
24
21
|
* - allOf[object, {__jsonSchema: {conditional: ...}}] -> object with if/then/else at top level
|
|
25
22
|
* - patternProperties meta -> patternProperties at current level
|
|
26
23
|
*/
|
|
27
|
-
function reconstructJsonSchema(schema) {
|
|
24
|
+
export function reconstructJsonSchema(schema) {
|
|
28
25
|
if (typeof schema !== "object" || schema === null) {
|
|
29
26
|
return schema;
|
|
30
27
|
}
|
package/dist/types/Types.d.ts
CHANGED
|
@@ -6,6 +6,11 @@ export type JsonSchemaObject = {
|
|
|
6
6
|
type?: string | string[];
|
|
7
7
|
$id?: string;
|
|
8
8
|
$ref?: string;
|
|
9
|
+
$anchor?: string;
|
|
10
|
+
$dynamicRef?: string;
|
|
11
|
+
$dynamicAnchor?: string;
|
|
12
|
+
$recursiveRef?: string;
|
|
13
|
+
$recursiveAnchor?: boolean;
|
|
9
14
|
$defs?: Record<string, JsonSchema>;
|
|
10
15
|
definitions?: Record<string, JsonSchema>;
|
|
11
16
|
title?: string;
|
|
@@ -53,9 +58,7 @@ export type JsonSchemaObject = {
|
|
|
53
58
|
errorMessage?: {
|
|
54
59
|
[key: string]: string | undefined;
|
|
55
60
|
};
|
|
56
|
-
} &
|
|
57
|
-
[key: string]: any;
|
|
58
|
-
};
|
|
61
|
+
} & Record<string, unknown>;
|
|
59
62
|
export type ParserSelector = (schema: JsonSchemaObject, refs: Refs) => string;
|
|
60
63
|
export type ParserOverride = (schema: JsonSchemaObject, refs: Refs) => string | void;
|
|
61
64
|
export type Options = {
|
|
@@ -99,6 +102,16 @@ export type Options = {
|
|
|
99
102
|
* Can be used to log or throw on unknown formats.
|
|
100
103
|
*/
|
|
101
104
|
onUnknownFormat?: (format: string, path: (string | number)[]) => void;
|
|
105
|
+
/**
|
|
106
|
+
* Called when a $ref/$dynamicRef cannot be resolved.
|
|
107
|
+
* Can be used to log or throw on unknown references.
|
|
108
|
+
*/
|
|
109
|
+
onUnresolvedRef?: (ref: string, path: (string | number)[]) => void;
|
|
110
|
+
/**
|
|
111
|
+
* Optional resolver for external $ref URIs.
|
|
112
|
+
* Return a JsonSchema to register, or undefined if not found.
|
|
113
|
+
*/
|
|
114
|
+
resolveExternalRef?: (uri: string) => JsonSchema | Promise<JsonSchema> | undefined;
|
|
102
115
|
};
|
|
103
116
|
export type Refs = Options & {
|
|
104
117
|
path: (string | number)[];
|
|
@@ -115,6 +128,24 @@ export type Refs = Options & {
|
|
|
115
128
|
currentSchemaName?: string;
|
|
116
129
|
cycleRefNames?: Set<string>;
|
|
117
130
|
cycleComponentByName?: Map<string, number>;
|
|
131
|
+
/** Base URI in scope while traversing */
|
|
132
|
+
currentBaseUri?: string;
|
|
133
|
+
/** Root/base URI for the document */
|
|
134
|
+
rootBaseUri?: string;
|
|
135
|
+
/** Prebuilt registry of resolved URIs/anchors */
|
|
136
|
+
refRegistry?: Map<string, {
|
|
137
|
+
schema: JsonSchema;
|
|
138
|
+
path: (string | number)[];
|
|
139
|
+
baseUri: string;
|
|
140
|
+
dynamic?: boolean;
|
|
141
|
+
anchor?: string;
|
|
142
|
+
}>;
|
|
143
|
+
/** Stack of active dynamic anchors (nearest last) */
|
|
144
|
+
dynamicAnchors?: {
|
|
145
|
+
name: string;
|
|
146
|
+
uri: string;
|
|
147
|
+
path: (string | number)[];
|
|
148
|
+
}[];
|
|
118
149
|
};
|
|
119
150
|
export type SimpleDiscriminatedOneOfSchema<D extends string = string> = JsonSchemaObject & {
|
|
120
151
|
oneOf: (JsonSchemaObject & {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Options, JsonSchema } from "../Types.js";
|
|
2
|
+
export type NormalizedOptions = Options & {
|
|
3
|
+
exportRefs: boolean;
|
|
4
|
+
withMeta: boolean;
|
|
5
|
+
};
|
|
6
|
+
export type AnalysisResult = {
|
|
7
|
+
schema: JsonSchema;
|
|
8
|
+
options: NormalizedOptions;
|
|
9
|
+
refNameByPointer: Map<string, string>;
|
|
10
|
+
usedNames: Set<string>;
|
|
11
|
+
declarations: Map<string, string>;
|
|
12
|
+
dependencies: Map<string, Set<string>>;
|
|
13
|
+
cycleRefNames: Set<string>;
|
|
14
|
+
cycleComponentByName: Map<string, number>;
|
|
15
|
+
refRegistry: Map<string, {
|
|
16
|
+
schema: JsonSchema;
|
|
17
|
+
path: (string | number)[];
|
|
18
|
+
baseUri: string;
|
|
19
|
+
dynamic?: boolean;
|
|
20
|
+
anchor?: string;
|
|
21
|
+
}>;
|
|
22
|
+
rootBaseUri: string;
|
|
23
|
+
};
|
|
24
|
+
export declare const analyzeSchema: (schema: JsonSchema, options?: Options) => AnalysisResult;
|
|
@@ -29,6 +29,11 @@ export type RefResolutionOptions = {
|
|
|
29
29
|
path: (string | number)[];
|
|
30
30
|
isCycle: boolean;
|
|
31
31
|
}) => RefResolutionResult | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* When true, cross-def references that participate in a cycle are emitted as z.lazy(() => Ref)
|
|
34
|
+
* to avoid TDZ issues across files.
|
|
35
|
+
*/
|
|
36
|
+
lazyCrossRefs?: boolean;
|
|
32
37
|
/** Called for unknown $refs (outside of $defs/definitions) */
|
|
33
38
|
onUnknownRef?: (ctx: {
|
|
34
39
|
ref: string;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export * from "./Types.js";
|
|
2
|
+
export * from "./core/analyzeSchema.js";
|
|
3
|
+
export * from "./core/emitZod.js";
|
|
2
4
|
export * from "./generators/generateBundle.js";
|
|
3
5
|
export * from "./jsonSchemaToZod.js";
|
|
4
6
|
export * from "./parsers/parseAllOf.js";
|
|
@@ -20,10 +22,12 @@ export * from "./parsers/parseSchema.js";
|
|
|
20
22
|
export * from "./parsers/parseSimpleDiscriminatedOneOf.js";
|
|
21
23
|
export * from "./parsers/parseString.js";
|
|
22
24
|
export * from "./utils/anyOrUnknown.js";
|
|
25
|
+
export * from "./utils/buildRefRegistry.js";
|
|
23
26
|
export * from "./utils/cycles.js";
|
|
24
27
|
export * from "./utils/half.js";
|
|
25
28
|
export * from "./utils/jsdocs.js";
|
|
26
29
|
export * from "./utils/omit.js";
|
|
30
|
+
export * from "./utils/resolveUri.js";
|
|
27
31
|
export * from "./utils/withMessage.js";
|
|
28
32
|
export * from "./zodToJsonSchema.js";
|
|
29
33
|
import { jsonSchemaToZod } from "./jsonSchemaToZod.js";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { Options, JsonSchema } from "./Types.js";
|
|
2
|
-
export declare const jsonSchemaToZod: (schema: JsonSchema,
|
|
2
|
+
export declare const jsonSchemaToZod: (schema: JsonSchema, options?: Options) => string;
|
|
@@ -29,7 +29,8 @@ export declare const its: {
|
|
|
29
29
|
not: JsonSchema;
|
|
30
30
|
};
|
|
31
31
|
ref: (x: JsonSchemaObject) => x is JsonSchemaObject & {
|
|
32
|
-
$ref
|
|
32
|
+
$ref?: string;
|
|
33
|
+
$dynamicRef?: string;
|
|
33
34
|
};
|
|
34
35
|
const: (x: JsonSchemaObject) => x is JsonSchemaObject & {
|
|
35
36
|
const: Serializable;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { JsonSchema } from "../Types.js";
|
|
2
|
+
export type RefRegistryEntry = {
|
|
3
|
+
schema: JsonSchema;
|
|
4
|
+
path: (string | number)[];
|
|
5
|
+
baseUri: string;
|
|
6
|
+
dynamic?: boolean;
|
|
7
|
+
anchor?: string;
|
|
8
|
+
};
|
|
9
|
+
export declare const buildRefRegistry: (schema: JsonSchema, rootBaseUri?: string) => {
|
|
10
|
+
registry: Map<string, RefRegistryEntry>;
|
|
11
|
+
rootBaseUri: string;
|
|
12
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const resolveUri: (base: string, ref: string) => string;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Schema Bundling Refactor (Analyzer + Emitters)
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
- `generateSchemaBundle` currently recurses via `parserOverride` and can overflow the stack when inline `$defs` are present (root hits immediately). Inline `$defs` inside `$defs` are also overwritten when stitching schemas.
|
|
5
|
+
- The conversion pipeline mixes concerns: parsing/analysis, code emission, and bundling strategy live together in `jsonSchemaToZod` and the bundle generator.
|
|
6
|
+
|
|
7
|
+
## Goals
|
|
8
|
+
- Single responsibility: analyze JsonSchema once, emit code through pluggable strategies (single file, bundle, nested types).
|
|
9
|
+
- Open for extension: new emitters (e.g., type-only), new ref resolution policies, without touching the analyzer.
|
|
10
|
+
- Safer bundling: no recursive parser overrides; import-aware ref resolution; preserve inline `$defs`.
|
|
11
|
+
- Testable units: analyzer IR and emitters have focused tests; bundle strategy tested with snapshots.
|
|
12
|
+
|
|
13
|
+
## Proposed Architecture
|
|
14
|
+
- **Analyzer (`analyzeSchema`)**: Convert JsonSchema + options into an intermediate representation (IR) containing symbols, ref pointer map, dependency graph, cycle info, and metadata flags. No code strings.
|
|
15
|
+
- **Emitters**:
|
|
16
|
+
- `emitZod(ir, emitOptions)`: IR → zod code (esm/cjs/none), with naming hooks and export policies.
|
|
17
|
+
- `emitTypes(ir, typeOptions)`: optional type-only exports (for nested types or barrel typing).
|
|
18
|
+
- **Strategies**:
|
|
19
|
+
- `SingleFileStrategy`: analyze root → emit zod once.
|
|
20
|
+
- `BundleStrategy`: analyze root once → slice IR per `$def` + root → emit per-file zod using an import-capable RefResolutionStrategy. Inline `$defs` remain scoped; cross-def `$ref`s become imports; unknown refs handled via policy.
|
|
21
|
+
- `NestedTypesStrategy`: walk IR titles/property paths to emit a dedicated types file.
|
|
22
|
+
- **Public API**:
|
|
23
|
+
- `analyzeSchema(schema, options): AnalysisResult`
|
|
24
|
+
- `emitZod(ir, emitOptions): string`
|
|
25
|
+
- `generateSchemaBundle(schema, bundleOptions): { files }` implemented via BundleStrategy
|
|
26
|
+
- `jsonSchemaToZod(schema, options): string` becomes a thin wrapper (analyze + emit single file).
|
|
27
|
+
|
|
28
|
+
## SOLID Alignment
|
|
29
|
+
- SRP: analyzer, emitter, strategy are separate modules.
|
|
30
|
+
- OCP: new emitters/strategies plug in without changing analyzer.
|
|
31
|
+
- LSP/ISP: narrow contracts (naming hooks, ref resolution hooks) instead of monolithic option bags.
|
|
32
|
+
- DIP: bundle strategy depends on IR abstractions, not on concrete `jsonSchemaToZod` string output.
|
|
33
|
+
|
|
34
|
+
## Migration Plan
|
|
35
|
+
1) **Foundations**: Extract analyzer + zod emitter modules; make `jsonSchemaToZod` call them. Preserve output parity and option validation. Add tests around analyzer/emitter.
|
|
36
|
+
2) **Bundle Strategy**: Rework `generateSchemaBundle` to use the analyzer IR and an import-aware ref strategy; remove recursive `parserOverride`; preserve inline `$defs` within defs.
|
|
37
|
+
3) **Nested Types**: Move nested type extraction to IR-based walker; emit via `emitTypes`.
|
|
38
|
+
4) **Cleanups & API polish**: Reduce option bag coupling; document new APIs; consider default export ergonomics.
|
|
39
|
+
|
|
40
|
+
## Risks / Mitigations
|
|
41
|
+
- Risk: Output regressions. Mitigation: snapshot tests for single-file and bundle outputs.
|
|
42
|
+
- Risk: Bundle import mapping errors. Mitigation: ref-strategy unit tests (cycles, unknown refs, cross-def).
|
|
43
|
+
- Risk: Incremental refactor churn. Mitigation: keep `jsonSchemaToZod` wrapper stable while internals shift; land in stages with tests.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Proposal: Robust `$ref` / `$id` / `$anchor` / `$dynamicRef` Support
|
|
2
|
+
|
|
3
|
+
## Goals
|
|
4
|
+
- Resolve `$ref` using full URI semantics (RFC 3986), not just `#/` pointers.
|
|
5
|
+
- Support `$id`/`$anchor`/`$dynamicAnchor`/`$dynamicRef` (and legacy `$recursiveRef/$recursiveAnchor`).
|
|
6
|
+
- Keep resolver logic in the analyzer/IR layer so emitters/strategies stay SOLID (SRP/OCP).
|
|
7
|
+
- Provide hooks for external schema resolution and unresolved-ref handling.
|
|
8
|
+
- Preserve existing `$defs`/JSON Pointer behavior for compatibility.
|
|
9
|
+
|
|
10
|
+
## Architecture alignment (with bundle refactor)
|
|
11
|
+
- Implement ref/anchor logic in the analyzer; emitters consume IR edges, not URIs.
|
|
12
|
+
- Define a pluggable `RefResolutionStrategy` used by the analyzer:
|
|
13
|
+
- Inputs: `ref`, `contextBaseUri`, `dynamicStack`, `registry`, optional `externalResolver`, `onUnresolvedRef`.
|
|
14
|
+
- Output: resolved IR node (or unresolved marker/fallback).
|
|
15
|
+
- Registry and dynamic stacks are built/maintained during analysis; IR carries resolved targets keyed by URI+fragment.
|
|
16
|
+
|
|
17
|
+
## Plan
|
|
18
|
+
|
|
19
|
+
### 1) Build a URI/anchor registry (analyzer prepass)
|
|
20
|
+
- Walk the schema once, tracking base URI (respect `$id`).
|
|
21
|
+
- Register base URI entries, `$anchor` (base#anchor), `$dynamicAnchor` (base#anchor, dynamic flag).
|
|
22
|
+
- Handle relative `$id` resolution per RFC 3986.
|
|
23
|
+
- Attach registry to IR/context.
|
|
24
|
+
|
|
25
|
+
### 2) URI-based ref resolution
|
|
26
|
+
- `resolveRef(ref, contextBaseUri, registry, dynamicStack)`:
|
|
27
|
+
- Resolve against `contextBaseUri` → absolute URI; split base/fragment.
|
|
28
|
+
- For `$dynamicRef`, search `dynamicStack` top-down for matching anchor; else fallback to registry lookup.
|
|
29
|
+
- For normal `$ref`, look up base+fragment in registry; empty fragment hits base entry.
|
|
30
|
+
- On miss: invoke `onUnresolvedRef` hook and return unresolved marker.
|
|
31
|
+
- Analyzer produces IR references keyed by resolved URI+fragment; name generation uses this key.
|
|
32
|
+
|
|
33
|
+
### 3) Thread base URI & dynamic stack in analyzer
|
|
34
|
+
- Extend analyzer traversal context (similar to Refs) with `currentBaseUri`, `dynamicAnchors`.
|
|
35
|
+
- On `$id`, compute new base; pass to children.
|
|
36
|
+
- On `$dynamicAnchor`, push onto stack for node scope; pop on exit.
|
|
37
|
+
- Emitters receive IR that already encodes resolved refs.
|
|
38
|
+
|
|
39
|
+
### 4) Legacy recursive keywords
|
|
40
|
+
- Treat `$recursiveAnchor` as a special dynamic anchor name.
|
|
41
|
+
- Treat `$recursiveRef` like `$dynamicRef` targeting that name.
|
|
42
|
+
|
|
43
|
+
### 5) External refs (optional, pluggable)
|
|
44
|
+
- Analyzer option `resolveExternalRef(uri)` (sync/async) to fetch external schemas.
|
|
45
|
+
- On external base URI miss, call resolver, prewalk and cache registry for that URI, then resolve.
|
|
46
|
+
- Guard against cycles with in-progress cache.
|
|
47
|
+
|
|
48
|
+
### 6) Naming & cycles
|
|
49
|
+
- Key ref names by resolved URI+fragment; store map in IR for consistent imports/aliases.
|
|
50
|
+
- Preserve cycle detection using these names.
|
|
51
|
+
|
|
52
|
+
### 7) Error/warning handling
|
|
53
|
+
- Option `onUnresolvedRef(uri, path)` for logging/throwing.
|
|
54
|
+
- Policy for fallback (`z.any()`/`z.unknown()` or error) lives in emitter/strategy but is driven by analyzer’s unresolved marker.
|
|
55
|
+
|
|
56
|
+
### 8) Tests
|
|
57
|
+
- Analyzer-level tests: `$id`/`$anchor` resolution (absolute/relative), `$dynamicAnchor`/`$dynamicRef` scoping, legacy recursive, external resolver stub, cycles, backward-compatible `#/` refs.
|
|
58
|
+
- Strategy/emitter tests: bundle imports for cross-file refs, naming stability with URI keys.
|
|
59
|
+
|
|
60
|
+
### 9) Migration steps
|
|
61
|
+
- Add registry prepass and URI resolver in analyzer.
|
|
62
|
+
- Thread `currentBaseUri`/`dynamicAnchors` through analysis context.
|
|
63
|
+
- Produce IR refs keyed by resolved URI; update naming map/cycle tracking.
|
|
64
|
+
- Add resolver hooks and unresolved handling.
|
|
65
|
+
- Add tests/fixtures; keep emitters unchanged except to consume new IR ref keys.
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import parser from "@typescript-eslint/parser";
|
|
2
|
+
import pluginTs from "@typescript-eslint/eslint-plugin";
|
|
3
|
+
|
|
4
|
+
export default [
|
|
5
|
+
{
|
|
6
|
+
ignores: ["dist", "node_modules", "test/output/**"],
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
files: ["**/*.ts", "**/*.tsx"],
|
|
10
|
+
languageOptions: {
|
|
11
|
+
parser,
|
|
12
|
+
parserOptions: {
|
|
13
|
+
ecmaVersion: "latest",
|
|
14
|
+
sourceType: "module",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
plugins: {
|
|
18
|
+
"@typescript-eslint": pluginTs,
|
|
19
|
+
},
|
|
20
|
+
rules: {
|
|
21
|
+
...pluginTs.configs.recommended.rules,
|
|
22
|
+
"@typescript-eslint/no-require-imports": "error",
|
|
23
|
+
"@typescript-eslint/no-var-requires": "error",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
];
|