@apidevtools/json-schema-ref-parser 15.3.0 → 15.3.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.
@@ -14,10 +14,17 @@ function bundle(parser, options) {
14
14
  // Build an inventory of all $ref pointers in the JSON Schema
15
15
  const inventory = [];
16
16
  crawl(parser, "schema", parser.$refs._root$Ref.path + "#", "#", 0, inventory, parser.$refs, options);
17
+ // Get the root schema's $id (if any) for qualifying refs inside sub-schemas with their own $id
18
+ const rootId = parser.schema && typeof parser.schema === "object" && "$id" in parser.schema
19
+ ? parser.schema.$id
20
+ : undefined;
17
21
  // Remap all $ref pointers
18
- remap(inventory, options);
22
+ remap(inventory, options, rootId);
19
23
  // Fix any $ref paths that traverse through other $refs (which is invalid per JSON Schema spec)
20
- fixRefsThroughRefs(inventory, parser.schema);
24
+ const bundleOptions = (options.bundle || {});
25
+ if (bundleOptions.optimizeInternalRefs !== false) {
26
+ fixRefsThroughRefs(inventory, parser.schema);
27
+ }
21
28
  }
22
29
  /**
23
30
  * Recursively crawls the given value, and inventories all JSON references.
@@ -157,7 +164,7 @@ function inventory$Ref($refParent, $refKey, path, pathFromRoot, indirections, in
157
164
  *
158
165
  * @param inventory
159
166
  */
160
- function remap(inventory, options) {
167
+ function remap(inventory, options, rootId) {
161
168
  // Group & sort all the $ref pointers, so they're in the order that we need to dereference/remap them
162
169
  inventory.sort((a, b) => {
163
170
  if (a.file !== b.file) {
@@ -202,17 +209,35 @@ function remap(inventory, options) {
202
209
  let file, hash, pathFromRoot;
203
210
  for (const entry of inventory) {
204
211
  // console.log('Re-mapping $ref pointer "%s" at %s', entry.$ref.$ref, entry.pathFromRoot);
212
+ const bundleOpts = (options.bundle || {});
205
213
  if (!entry.external) {
206
- // This $ref already resolves to the main JSON Schema file
207
- entry.$ref.$ref = entry.hash;
214
+ // This $ref already resolves to the main JSON Schema file.
215
+ // When optimizeInternalRefs is false, preserve the original internal ref path
216
+ // instead of rewriting it to the fully resolved hash.
217
+ if (bundleOpts.optimizeInternalRefs !== false) {
218
+ entry.$ref.$ref = entry.hash;
219
+ }
208
220
  }
209
221
  else if (entry.file === file && entry.hash === hash) {
210
222
  // This $ref points to the same value as the previous $ref, so remap it to the same path
211
- entry.$ref.$ref = pathFromRoot;
223
+ if (rootId && isInsideIdScope(inventory, entry)) {
224
+ // This entry is inside a sub-schema with its own $id, so a bare root-relative JSON Pointer
225
+ // would be resolved relative to that $id, not the document root. Qualify with the root $id.
226
+ entry.$ref.$ref = rootId + pathFromRoot;
227
+ }
228
+ else {
229
+ entry.$ref.$ref = pathFromRoot;
230
+ }
212
231
  }
213
232
  else if (entry.file === file && entry.hash.indexOf(hash + "/") === 0) {
214
233
  // This $ref points to a sub-value of the previous $ref, so remap it beneath that path
215
- entry.$ref.$ref = Pointer.join(pathFromRoot, Pointer.parse(entry.hash.replace(hash, "#")));
234
+ const subPath = Pointer.join(pathFromRoot, Pointer.parse(entry.hash.replace(hash, "#")));
235
+ if (rootId && isInsideIdScope(inventory, entry)) {
236
+ entry.$ref.$ref = rootId + subPath;
237
+ }
238
+ else {
239
+ entry.$ref.$ref = subPath;
240
+ }
216
241
  }
217
242
  else {
218
243
  // We've moved to a new file or new hash
@@ -339,4 +364,25 @@ function walkPath(schema, path) {
339
364
  }
340
365
  return current;
341
366
  }
367
+ /**
368
+ * Checks whether the given inventory entry is located inside a sub-schema that has its own $id.
369
+ * If so, root-relative JSON Pointer $refs placed at this location would be resolved against
370
+ * the $id base URI rather than the document root, making them invalid.
371
+ */
372
+ function isInsideIdScope(inventory, entry) {
373
+ for (const other of inventory) {
374
+ // Skip root-level entries
375
+ if (other.pathFromRoot === "#" || other.pathFromRoot === "#/") {
376
+ continue;
377
+ }
378
+ // Check if the other entry is an ancestor of the current entry
379
+ if (entry.pathFromRoot.startsWith(other.pathFromRoot + "/")) {
380
+ // Check if the ancestor's resolved value has a $id
381
+ if (other.value && typeof other.value === "object" && "$id" in other.value) {
382
+ return true;
383
+ }
384
+ }
385
+ }
386
+ return false;
387
+ }
342
388
  export default bundle;
@@ -83,7 +83,13 @@ function crawl(obj, path, pathFromRoot, parents, processedObjects, dereferencedC
83
83
  });
84
84
  }
85
85
  }
86
- obj[key] = dereferenced.value;
86
+ // Clone the dereferenced value if cloneReferences is enabled and this is not a
87
+ // circular reference. This prevents mutations to one location from affecting others.
88
+ let assignedValue = dereferenced.value;
89
+ if (derefOptions?.cloneReferences && !circular && assignedValue && typeof assignedValue === "object") {
90
+ assignedValue = structuredClone(assignedValue);
91
+ }
92
+ obj[key] = assignedValue;
87
93
  // If we have data to preserve and our dereferenced object is still an object then
88
94
  // we need copy back our preserved data into our dereferenced schema.
89
95
  if (derefOptions?.preservedProperties) {
@@ -18,6 +18,14 @@ export interface BundleOptions {
18
18
  * @argument {string} parentPropName - The prop name of the parent object whose value was processed
19
19
  */
20
20
  onBundle?(path: string, value: JSONSchemaObject, parent?: JSONSchemaObject, parentPropName?: string): void;
21
+ /**
22
+ * Whether to optimize internal `$ref` paths by following intermediate `$ref` chains and
23
+ * rewriting them to point directly to the final target. When `false`, intermediate `$ref`
24
+ * indirections are preserved as-is.
25
+ *
26
+ * Default: `true`
27
+ */
28
+ optimizeInternalRefs?: boolean;
21
29
  }
22
30
  export interface DereferenceOptions {
23
31
  /**
@@ -78,6 +86,20 @@ export interface DereferenceOptions {
78
86
  * Default: 500
79
87
  */
80
88
  maxDepth?: number;
89
+ /**
90
+ * Whether to create independent clones of each `$ref` target value instead of
91
+ * reusing the same JS object reference. When `false` (the default), multiple
92
+ * `$ref` pointers that resolve to the same value will all share the same object
93
+ * in memory, so modifying one will affect all others. When `true`, each `$ref`
94
+ * replacement gets its own deep copy, preventing unintended side effects from
95
+ * post-dereference mutations.
96
+ *
97
+ * Note: circular references are never cloned — they always maintain reference
98
+ * equality to correctly represent the circular structure.
99
+ *
100
+ * Default: `false`
101
+ */
102
+ cloneReferences?: boolean;
81
103
  }
82
104
  /**
83
105
  * Options that determine how JSON schemas are parsed, resolved, and dereferenced.
package/lib/bundle.ts CHANGED
@@ -39,11 +39,20 @@ function bundle<S extends object = JSONSchema, O extends ParserOptions<S> = Pars
39
39
  const inventory: InventoryEntry[] = [];
40
40
  crawl<S, O>(parser, "schema", parser.$refs._root$Ref.path + "#", "#", 0, inventory, parser.$refs, options);
41
41
 
42
+ // Get the root schema's $id (if any) for qualifying refs inside sub-schemas with their own $id
43
+ const rootId =
44
+ parser.schema && typeof parser.schema === "object" && "$id" in (parser.schema as any)
45
+ ? (parser.schema as any).$id
46
+ : undefined;
47
+
42
48
  // Remap all $ref pointers
43
- remap<S, O>(inventory, options);
49
+ remap<S, O>(inventory, options, rootId);
44
50
 
45
51
  // Fix any $ref paths that traverse through other $refs (which is invalid per JSON Schema spec)
46
- fixRefsThroughRefs(inventory, parser.schema as any);
52
+ const bundleOptions = (options.bundle || {}) as BundleOptions;
53
+ if (bundleOptions.optimizeInternalRefs !== false) {
54
+ fixRefsThroughRefs(inventory, parser.schema as any);
55
+ }
47
56
  }
48
57
 
49
58
  /**
@@ -209,6 +218,7 @@ function inventory$Ref<S extends object = JSONSchema, O extends ParserOptions<S>
209
218
  function remap<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(
210
219
  inventory: InventoryEntry[],
211
220
  options: O,
221
+ rootId?: string,
212
222
  ) {
213
223
  // Group & sort all the $ref pointers, so they're in the order that we need to dereference/remap them
214
224
  inventory.sort((a: InventoryEntry, b: InventoryEntry) => {
@@ -256,15 +266,31 @@ function remap<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
256
266
  for (const entry of inventory) {
257
267
  // console.log('Re-mapping $ref pointer "%s" at %s', entry.$ref.$ref, entry.pathFromRoot);
258
268
 
269
+ const bundleOpts = (options.bundle || {}) as BundleOptions;
259
270
  if (!entry.external) {
260
- // This $ref already resolves to the main JSON Schema file
261
- entry.$ref.$ref = entry.hash;
271
+ // This $ref already resolves to the main JSON Schema file.
272
+ // When optimizeInternalRefs is false, preserve the original internal ref path
273
+ // instead of rewriting it to the fully resolved hash.
274
+ if (bundleOpts.optimizeInternalRefs !== false) {
275
+ entry.$ref.$ref = entry.hash;
276
+ }
262
277
  } else if (entry.file === file && entry.hash === hash) {
263
278
  // This $ref points to the same value as the previous $ref, so remap it to the same path
264
- entry.$ref.$ref = pathFromRoot;
279
+ if (rootId && isInsideIdScope(inventory, entry)) {
280
+ // This entry is inside a sub-schema with its own $id, so a bare root-relative JSON Pointer
281
+ // would be resolved relative to that $id, not the document root. Qualify with the root $id.
282
+ entry.$ref.$ref = rootId + pathFromRoot;
283
+ } else {
284
+ entry.$ref.$ref = pathFromRoot;
285
+ }
265
286
  } else if (entry.file === file && entry.hash.indexOf(hash + "/") === 0) {
266
287
  // This $ref points to a sub-value of the previous $ref, so remap it beneath that path
267
- entry.$ref.$ref = Pointer.join(pathFromRoot, Pointer.parse(entry.hash.replace(hash, "#")));
288
+ const subPath = Pointer.join(pathFromRoot, Pointer.parse(entry.hash.replace(hash, "#")));
289
+ if (rootId && isInsideIdScope(inventory, entry)) {
290
+ entry.$ref.$ref = rootId + subPath;
291
+ } else {
292
+ entry.$ref.$ref = subPath;
293
+ }
268
294
  } else {
269
295
  // We've moved to a new file or new hash
270
296
  file = entry.file;
@@ -409,4 +435,26 @@ function walkPath(schema: any, path: string): any {
409
435
  return current;
410
436
  }
411
437
 
438
+ /**
439
+ * Checks whether the given inventory entry is located inside a sub-schema that has its own $id.
440
+ * If so, root-relative JSON Pointer $refs placed at this location would be resolved against
441
+ * the $id base URI rather than the document root, making them invalid.
442
+ */
443
+ function isInsideIdScope(inventory: InventoryEntry[], entry: InventoryEntry): boolean {
444
+ for (const other of inventory) {
445
+ // Skip root-level entries
446
+ if (other.pathFromRoot === "#" || other.pathFromRoot === "#/") {
447
+ continue;
448
+ }
449
+ // Check if the other entry is an ancestor of the current entry
450
+ if (entry.pathFromRoot.startsWith(other.pathFromRoot + "/")) {
451
+ // Check if the ancestor's resolved value has a $id
452
+ if (other.value && typeof other.value === "object" && "$id" in other.value) {
453
+ return true;
454
+ }
455
+ }
456
+ }
457
+ return false;
458
+ }
459
+
412
460
  export default bundle;
@@ -146,7 +146,14 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
146
146
  }
147
147
  }
148
148
 
149
- obj[key] = dereferenced.value;
149
+ // Clone the dereferenced value if cloneReferences is enabled and this is not a
150
+ // circular reference. This prevents mutations to one location from affecting others.
151
+ let assignedValue = dereferenced.value;
152
+ if (derefOptions?.cloneReferences && !circular && assignedValue && typeof assignedValue === "object") {
153
+ assignedValue = structuredClone(assignedValue);
154
+ }
155
+
156
+ obj[key] = assignedValue;
150
157
 
151
158
  // If we have data to preserve and our dereferenced object is still an object then
152
159
  // we need copy back our preserved data into our dereferenced schema.
package/lib/options.ts CHANGED
@@ -30,6 +30,15 @@ export interface BundleOptions {
30
30
  * @argument {string} parentPropName - The prop name of the parent object whose value was processed
31
31
  */
32
32
  onBundle?(path: string, value: JSONSchemaObject, parent?: JSONSchemaObject, parentPropName?: string): void;
33
+
34
+ /**
35
+ * Whether to optimize internal `$ref` paths by following intermediate `$ref` chains and
36
+ * rewriting them to point directly to the final target. When `false`, intermediate `$ref`
37
+ * indirections are preserved as-is.
38
+ *
39
+ * Default: `true`
40
+ */
41
+ optimizeInternalRefs?: boolean;
33
42
  }
34
43
 
35
44
  export interface DereferenceOptions {
@@ -98,6 +107,21 @@ export interface DereferenceOptions {
98
107
  * Default: 500
99
108
  */
100
109
  maxDepth?: number;
110
+
111
+ /**
112
+ * Whether to create independent clones of each `$ref` target value instead of
113
+ * reusing the same JS object reference. When `false` (the default), multiple
114
+ * `$ref` pointers that resolve to the same value will all share the same object
115
+ * in memory, so modifying one will affect all others. When `true`, each `$ref`
116
+ * replacement gets its own deep copy, preventing unintended side effects from
117
+ * post-dereference mutations.
118
+ *
119
+ * Note: circular references are never cloned — they always maintain reference
120
+ * equality to correctly represent the circular structure.
121
+ *
122
+ * Default: `false`
123
+ */
124
+ cloneReferences?: boolean;
101
125
  }
102
126
 
103
127
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apidevtools/json-schema-ref-parser",
3
- "version": "15.3.0",
3
+ "version": "15.3.1",
4
4
  "description": "Parse, Resolve, and Dereference JSON Schema $ref pointers",
5
5
  "type": "module",
6
6
  "types": "dist/lib/index.d.ts",