@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.
Files changed (51) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/dist/cjs/core/analyzeSchema.js +62 -0
  3. package/dist/cjs/core/emitZod.js +141 -0
  4. package/dist/cjs/generators/generateBundle.js +355 -0
  5. package/dist/cjs/index.js +6 -0
  6. package/dist/cjs/jsonSchemaToZod.js +5 -73
  7. package/dist/cjs/parsers/parseArray.js +34 -15
  8. package/dist/cjs/parsers/parseIfThenElse.js +2 -1
  9. package/dist/cjs/parsers/parseNumber.js +81 -39
  10. package/dist/cjs/parsers/parseObject.js +24 -0
  11. package/dist/cjs/parsers/parseSchema.js +147 -25
  12. package/dist/cjs/parsers/parseString.js +294 -54
  13. package/dist/cjs/utils/buildRefRegistry.js +56 -0
  14. package/dist/cjs/utils/cycles.js +113 -0
  15. package/dist/cjs/utils/resolveUri.js +16 -0
  16. package/dist/cjs/utils/withMessage.js +4 -5
  17. package/dist/esm/core/analyzeSchema.js +58 -0
  18. package/dist/esm/core/emitZod.js +137 -0
  19. package/dist/esm/generators/generateBundle.js +351 -0
  20. package/dist/esm/index.js +6 -0
  21. package/dist/esm/jsonSchemaToZod.js +5 -73
  22. package/dist/esm/parsers/parseArray.js +34 -15
  23. package/dist/esm/parsers/parseIfThenElse.js +2 -1
  24. package/dist/esm/parsers/parseNumber.js +81 -39
  25. package/dist/esm/parsers/parseObject.js +24 -0
  26. package/dist/esm/parsers/parseSchema.js +147 -25
  27. package/dist/esm/parsers/parseString.js +294 -54
  28. package/dist/esm/utils/buildRefRegistry.js +52 -0
  29. package/dist/esm/utils/cycles.js +107 -0
  30. package/dist/esm/utils/resolveUri.js +12 -0
  31. package/dist/esm/utils/withMessage.js +4 -5
  32. package/dist/types/Types.d.ts +36 -0
  33. package/dist/types/core/analyzeSchema.d.ts +24 -0
  34. package/dist/types/core/emitZod.d.ts +2 -0
  35. package/dist/types/generators/generateBundle.d.ts +62 -0
  36. package/dist/types/index.d.ts +6 -0
  37. package/dist/types/jsonSchemaToZod.d.ts +1 -1
  38. package/dist/types/parsers/parseSchema.d.ts +2 -1
  39. package/dist/types/parsers/parseString.d.ts +2 -2
  40. package/dist/types/utils/buildRefRegistry.d.ts +12 -0
  41. package/dist/types/utils/cycles.d.ts +7 -0
  42. package/dist/types/utils/jsdocs.d.ts +1 -1
  43. package/dist/types/utils/resolveUri.d.ts +1 -0
  44. package/dist/types/utils/withMessage.d.ts +6 -1
  45. package/docs/proposals/bundle-refactor.md +43 -0
  46. package/docs/proposals/ref-anchor-support.md +65 -0
  47. package/eslint.config.js +26 -0
  48. package/package.json +10 -4
  49. /package/{jest.config.js → jest.config.cjs} +0 -0
  50. /package/{postcjs.js → postcjs.cjs} +0 -0
  51. /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 parseSchema_js_1 = require("./parsers/parseSchema.js");
5
- const jsdocs_js_1 = require("./utils/jsdocs.js");
6
- const jsonSchemaToZod = (schema, { module, name, type, noImport, ...rest } = {}) => {
7
- if (type && (!name || module !== "esm")) {
8
- throw new Error("Option `type` requires `name` to be set and `module` to be `esm`");
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
- r += (0, withMessage_js_1.withMessage)(schema, "maxItems", ({ json }) => [
41
- `.max(${json}`,
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 += (0, withMessage_js_1.withMessage)(schema, "uniqueItems", () => [
47
- ".unique(",
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.errors.forEach((error) => ctx.addIssue(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
- let r = "z.number()";
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
- r += (0, withMessage_js_1.withMessage)(schema, "type", () => [".int(", ")"]);
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
- r += (0, withMessage_js_1.withMessage)(schema, "format", ({ value }) => {
12
- if (value === "int64") {
13
- return [".int(", ")"];
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 [".int(", ")"];
48
+ return {
49
+ opener: ".int(",
50
+ closer: ")",
51
+ messagePrefix: "{ error: ",
52
+ messageCloser: " })",
53
+ };
23
54
  }
24
- return [`.multipleOf(${json}`, ", ", ")"];
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 resolved = resolveRef(refs.root, schema.$ref);
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(schema.$ref, path, refs);
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 = (root, ref) => {
114
- if (!root || !ref.startsWith("#/"))
115
- return undefined;
116
- const rawSegments = ref
117
- .slice(2)
118
- .split("/")
119
- .filter((segment) => segment.length > 0)
120
- .map(decodePointerSegment);
121
- let current = root;
122
- for (const segment of rawSegments) {
123
- if (typeof current !== "object" || current === null)
124
- return undefined;
125
- current = current[segment];
126
- }
127
- return { schema: current, path: rawSegments };
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.filter((segment) => segment !== "$defs" && segment !== "definitions" && segment !== "properties");
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),