···5858 instance
5959- Compatible with Node LTS and beyond, and all major web browsers on Windows, Mac, and Linux
60606161-## Example
6262-6363-```javascript
6464-import { $RefParser } from '@hey-api/json-schema-ref-parser';
6565-6666-try {
6767- const parser = new $RefParser();
6868- await parser.dereference({ pathOrUrlOrSchema: mySchema });
6969- console.log(parser.schema.definitions.person.properties.firstName);
7070-} catch (err) {
7171- console.error(err);
7272-}
7373-```
7474-7561### New in this fork (@hey-api)
76627763- **Multiple inputs with `bundleMany`**: Merge and bundle several OpenAPI/JSON Schema inputs (files, URLs, or raw objects) into a single schema. Components are prefixed to avoid name collisions, paths are namespaced on conflict, and `$ref`s are rewritten accordingly.
+20-122
packages/json-schema-ref-parser/src/bundle.ts
···44import $Ref from './ref';
55import type $Refs from './refs';
66import type { JSONSchema } from './types';
77+import { MissingPointerError } from './util/errors';
78import * as url from './util/url';
8999-const DEBUG_PERFORMANCE =
1010- process.env.DEBUG === 'true' ||
1111- (typeof globalThis !== 'undefined' && (globalThis as any).DEBUG_BUNDLE_PERFORMANCE === true);
1212-1313-const perf = {
1414- log: (message: string, ...args: any[]) =>
1515- DEBUG_PERFORMANCE && console.log('[PERF] ' + message, ...args),
1616- mark: (name: string) => DEBUG_PERFORMANCE && performance.mark(name),
1717- measure: (name: string, start: string, end: string) =>
1818- DEBUG_PERFORMANCE && performance.measure(name, start, end),
1919- warn: (message: string, ...args: any[]) =>
2020- DEBUG_PERFORMANCE && console.warn('[PERF] ' + message, ...args),
2121-};
2222-2310export interface InventoryEntry {
2411 $ref: any;
2512 circular: any;
···4330 const lookup = new Map<string, InventoryEntry>();
4431 const objectIds = new WeakMap<object, string>(); // Use WeakMap to avoid polluting objects
4532 let idCounter = 0;
4646- let lookupCount = 0;
4747- let addCount = 0;
48334934 const getObjectId = (obj: any) => {
5035 if (!objectIds.has(obj)) {
···59446045 return {
6146 add: (entry: InventoryEntry) => {
6262- addCount++;
6347 const key = createInventoryKey(entry.parent, entry.key);
6448 lookup.set(key, entry);
6565- if (addCount % 100 === 0) {
6666- perf.log(`Inventory lookup: Added ${addCount} entries, map size: ${lookup.size}`);
6767- }
6849 },
6950 find: ($refParent: any, $refKey: any) => {
7070- lookupCount++;
7151 const key = createInventoryKey($refParent, $refKey);
7252 const result = lookup.get(key);
7373- if (lookupCount % 100 === 0) {
7474- perf.log(`Inventory lookup: ${lookupCount} lookups performed`);
7575- }
7653 return result;
7754 },
7878- getStats: () => ({ addCount, lookupCount, mapSize: lookup.size }),
7955 remove: (entry: InventoryEntry) => {
8056 const key = createInventoryKey(entry.parent, entry.key);
8157 lookup.delete(key);
···171147 */
172148 visitedObjects?: WeakSet<object>;
173149}) => {
174174- perf.mark('inventory-ref-start');
175150 const $ref = $refKey === null ? $refParent : $refParent[$refKey];
176151 const $refPath = url.resolve(path, $ref.$ref);
177152178153 // Check cache first to avoid redundant resolution
179154 let pointer = resolvedRefs.get($refPath);
180155 if (!pointer) {
181181- perf.mark('resolve-start');
182182- pointer = $refs._resolve($refPath, pathFromRoot, options);
183183- perf.mark('resolve-end');
184184- perf.measure('resolve-time', 'resolve-start', 'resolve-end');
156156+ try {
157157+ pointer = $refs._resolve($refPath, pathFromRoot, options);
158158+ } catch (error) {
159159+ if (error instanceof MissingPointerError) {
160160+ // Log warning but continue - common in complex schema ecosystems
161161+ console.warn(`Skipping unresolvable $ref: ${$refPath}`);
162162+ return;
163163+ }
164164+ throw error; // Re-throw unexpected errors
165165+ }
185166186167 if (pointer) {
187168 resolvedRefs.set($refPath, pointer);
188188- perf.log(`Cached resolved $ref: ${$refPath}`);
189169 }
190170 }
191171192192- if (pointer === null) {
193193- perf.mark('inventory-ref-end');
194194- perf.measure('inventory-ref-time', 'inventory-ref-start', 'inventory-ref-end');
195195- return;
196196- }
172172+ if (pointer === null) return;
197173198174 const parsed = Pointer.parse(pathFromRoot);
199175 const depth = parsed.length;
···204180 indirections += pointer.indirections;
205181206182 // Check if this exact location (parent + key + pathFromRoot) has already been inventoried
207207- perf.mark('lookup-start');
208183 const existingEntry = inventoryLookup.find($refParent, $refKey);
209209- perf.mark('lookup-end');
210210- perf.measure('lookup-time', 'lookup-start', 'lookup-end');
211184212185 if (existingEntry && existingEntry.pathFromRoot === pathFromRoot) {
213186 // This exact location has already been inventoried, so we don't need to process it again
···215188 removeFromInventory(inventory, existingEntry);
216189 inventoryLookup.remove(existingEntry);
217190 } else {
218218- perf.mark('inventory-ref-end');
219219- perf.measure('inventory-ref-time', 'inventory-ref-start', 'inventory-ref-end');
220191 return;
221192 }
222193 }
···246217 inventory.push(newEntry);
247218 inventoryLookup.add(newEntry);
248219249249- perf.log(
250250- `Inventoried $ref: ${$ref.$ref} -> ${file}${hash} (external: ${external}, depth: ${depth})`,
251251- );
252252-253220 // Recursively crawl the resolved value
254221 if (!existingEntry || external) {
255255- perf.mark('crawl-recursive-start');
256222 crawl({
257223 $refs,
258224 indirections: indirections + 1,
···266232 resolvedRefs,
267233 visitedObjects,
268234 });
269269- perf.mark('crawl-recursive-end');
270270- perf.measure('crawl-recursive-time', 'crawl-recursive-start', 'crawl-recursive-end');
271235 }
272272-273273- perf.mark('inventory-ref-end');
274274- perf.measure('inventory-ref-time', 'inventory-ref-start', 'inventory-ref-end');
275236};
276237277238/**
···330291331292 if (obj && typeof obj === 'object' && !ArrayBuffer.isView(obj)) {
332293 // Early exit if we've already processed this exact object
333333- if (visitedObjects.has(obj)) {
334334- perf.log(`Skipping already visited object at ${pathFromRoot}`);
335335- return;
336336- }
294294+ if (visitedObjects.has(obj)) return;
337295338296 if ($Ref.isAllowed$Ref(obj)) {
339339- perf.log(`Found $ref at ${pathFromRoot}: ${(obj as any).$ref}`);
340297 inventory$Ref({
341298 $refKey: key,
342299 $refParent: parent,
···369326 // This produces the shortest possible bundled references
370327 return a.length - b.length;
371328 }
372372- }) as (keyof typeof obj)[];
329329+ }) as Array<keyof typeof obj>;
373330374331 for (const key of keys) {
375332 const keyPath = Pointer.join(path, key);
···414371 * Remap external refs by hoisting resolved values into a shared container in the root schema
415372 * and pointing all occurrences to those internal definitions. Internal refs remain internal.
416373 */
417417-function remap(parser: $RefParser, inventory: InventoryEntry[]) {
418418- perf.log(`Starting remap with ${inventory.length} inventory entries`);
419419- perf.mark('remap-start');
374374+function remap(parser: $RefParser, inventory: Array<InventoryEntry>) {
420375 const root = parser.schema as any;
421376422377 // Group & sort all the $ref pointers, so they're in the order that we need to dereference/remap them
423423- perf.mark('sort-inventory-start');
424378 inventory.sort((a: InventoryEntry, b: InventoryEntry) => {
425379 if (a.file !== b.file) {
426380 // Group all the $refs that point to the same file
···454408 }
455409 }
456410 });
457457-458458- perf.mark('sort-inventory-end');
459459- perf.measure('sort-inventory-time', 'sort-inventory-start', 'sort-inventory-end');
460460-461461- perf.log(`Sorted ${inventory.length} inventory entries`);
462411463412 // Ensure or return a container by component type. Prefer OpenAPI-aware placement;
464413 // otherwise use existing root containers; otherwise create components/*.
···583532 used.add(name);
584533 return name;
585534 };
586586- perf.mark('remap-loop-start');
587535 for (const entry of inventory) {
588536 // Safety check: ensure entry and entry.$ref are valid objects
589537 if (!entry || !entry.$ref || typeof entry.$ref !== 'object') {
590590- perf.warn(`Skipping invalid inventory entry:`, entry);
591538 continue;
592539 }
593540···654601 entry.parent[entry.key] = { $ref: refPath };
655602 }
656603 }
657657- perf.mark('remap-loop-end');
658658- perf.measure('remap-loop-time', 'remap-loop-start', 'remap-loop-end');
659659-660660- perf.mark('remap-end');
661661- perf.measure('remap-total-time', 'remap-start', 'remap-end');
662662-663663- perf.log(`Completed remap of ${inventory.length} entries`);
664604}
665605666666-function removeFromInventory(inventory: InventoryEntry[], entry: any) {
606606+function removeFromInventory(inventory: Array<InventoryEntry>, entry: any) {
667607 const index = inventory.indexOf(entry);
668608 inventory.splice(index, 1);
669609}
···676616 * @param parser
677617 * @param options
678618 */
679679-export const bundle = (parser: $RefParser, options: ParserOptions) => {
680680- // console.log('Bundling $ref pointers in %s', parser.$refs._root$Ref.path);
681681- perf.mark('bundle-start');
682682-683683- // Build an inventory of all $ref pointers in the JSON Schema
684684- const inventory: InventoryEntry[] = [];
619619+export function bundle(parser: $RefParser, options: ParserOptions): void {
620620+ const inventory: Array<InventoryEntry> = [];
685621 const inventoryLookup = createInventoryLookup();
686622687687- perf.log('Starting crawl phase');
688688- perf.mark('crawl-phase-start');
689689-690623 const visitedObjects = new WeakSet<object>();
691691- const resolvedRefs = new Map<string, any>(); // Cache for resolved $ref targets
624624+ const resolvedRefs = new Map<string, any>();
692625693626 crawl<JSONSchema>({
694627 $refs: parser.$refs,
···704637 visitedObjects,
705638 });
706639707707- perf.mark('crawl-phase-end');
708708- perf.measure('crawl-phase-time', 'crawl-phase-start', 'crawl-phase-end');
709709-710710- const stats = inventoryLookup.getStats();
711711- perf.log(`Crawl phase complete. Found ${inventory.length} $refs. Lookup stats:`, stats);
712712-713713- // Remap all $ref pointers
714714- perf.log('Starting remap phase');
715715- perf.mark('remap-phase-start');
716640 remap(parser, inventory);
717717- perf.mark('remap-phase-end');
718718- perf.measure('remap-phase-time', 'remap-phase-start', 'remap-phase-end');
719719-720720- perf.mark('bundle-end');
721721- perf.measure('bundle-total-time', 'bundle-start', 'bundle-end');
722722-723723- perf.log('Bundle complete. Performance summary:');
724724-725725- // Log final stats
726726- const finalStats = inventoryLookup.getStats();
727727- perf.log(`Final inventory stats:`, finalStats);
728728- perf.log(`Resolved refs cache size: ${resolvedRefs.size}`);
729729-730730- if (DEBUG_PERFORMANCE) {
731731- // Log all performance measures
732732- const measures = performance.getEntriesByType('measure');
733733- measures.forEach((measure) => {
734734- if (measure.name.includes('time')) {
735735- console.log(`${measure.name}: ${measure.duration.toFixed(2)}ms`);
736736- }
737737- });
738738-739739- // Clear performance marks and measures for next run
740740- performance.clearMarks();
741741- performance.clearMeasures();
742742- }
743743-};
641641+}
···11import { ono } from '@jsdevtools/ono';
2233-import type { $RefParser } from '.';
43import type { DereferenceOptions, ParserOptions } from './options';
54import Pointer from './pointer';
65import $Ref from './ref';
···87import type { JSONSchema } from './types';
98import { TimeoutError } from './util/errors';
109import * as url from './util/url';
1111-1212-export default dereference;
1313-1414-/**
1515- * Crawls the JSON schema, finds all JSON references, and dereferences them.
1616- * This method mutates the JSON schema object, replacing JSON references with their resolved value.
1717- *
1818- * @param parser
1919- * @param options
2020- */
2121-function dereference(parser: $RefParser, options: ParserOptions) {
2222- const start = Date.now();
2323- // console.log('Dereferencing $ref pointers in %s', parser.$refs._root$Ref.path);
2424- const dereferenced = crawl<JSONSchema>(
2525- parser.schema,
2626- parser.$refs._root$Ref.path!,
2727- '#',
2828- new Set(),
2929- new Set(),
3030- new Map(),
3131- parser.$refs,
3232- options,
3333- start,
3434- );
3535- parser.$refs.circular = dereferenced.circular;
3636- parser.schema = dereferenced.value;
3737-}
38103911/**
4012 * Recursively crawls the given value, and dereferences any JSON references.
+13-49
packages/json-schema-ref-parser/src/index.ts
···11import { ono } from '@jsdevtools/ono';
2233import { bundle as _bundle } from './bundle';
44-import _dereference from './dereference';
54import { getJsonSchemaRefParserDefaultOptions } from './options';
65import { newFile, parseFile } from './parse';
76import $Refs from './refs';
···1817 type: 'file' | 'json' | 'url';
1918}
20192121-export const getResolvedInput = ({
2020+export function getResolvedInput({
2221 pathOrUrlOrSchema,
2322}: {
2423 pathOrUrlOrSchema: JSONSchema | string | unknown;
2525-}): ResolvedInput => {
2424+}): ResolvedInput {
2625 if (!pathOrUrlOrSchema) {
2726 throw ono(`Expected a file path, URL, or object. Got ${pathOrUrlOrSchema}`);
2827 }
···6160 }
62616362 return resolvedInput;
6464-};
6363+}
65646665// NOTE: previously used helper removed as unused
6766···163162 }
164163165164 /**
166166- * Dereferences all `$ref` pointers in the JSON Schema, replacing each reference with its resolved value. This results in a schema object that does not contain any `$ref` pointers. Instead, it's a normal JavaScript object tree that can easily be crawled and used just like any other JavaScript object. This is great for programmatic usage, especially when using tools that don't understand JSON references.
167167- *
168168- * The dereference method maintains object reference equality, meaning that all `$ref` pointers that point to the same object will be replaced with references to the same object. Again, this is great for programmatic usage, but it does introduce the risk of circular references, so be careful if you intend to serialize the schema using `JSON.stringify()`. Consider using the bundle method instead, which does not create circular references.
169169- *
170170- * See https://apitools.dev/json-schema-ref-parser/docs/ref-parser.html#dereferenceschema-options-callback
171171- *
172172- * @param pathOrUrlOrSchema A JSON Schema object, or the file path or URL of a JSON Schema file.
173173- */
174174- public async dereference({
175175- fetch,
176176- pathOrUrlOrSchema,
177177- }: {
178178- fetch?: RequestInit;
179179- pathOrUrlOrSchema: JSONSchema | string | unknown;
180180- }): Promise<JSONSchema> {
181181- await this.parse({
182182- fetch,
183183- pathOrUrlOrSchema,
184184- });
185185- await resolveExternal(this, this.options);
186186- const errors = JSONParserErrorGroup.getParserErrors(this);
187187- if (errors.length > 0) {
188188- throw new JSONParserErrorGroup(this);
189189- }
190190- _dereference(this, this.options);
191191- const errors2 = JSONParserErrorGroup.getParserErrors(this);
192192- if (errors2.length > 0) {
193193- throw new JSONParserErrorGroup(this);
194194- }
195195- return this.schema!;
196196- }
197197-198198- /**
199165 * Parses the given JSON schema.
200166 * This method does not resolve any JSON references.
201167 * It just reads a single file in JSON or YAML format, and parse it as a JavaScript object.
···241207 fetch,
242208 file,
243209 });
244244- const parseResult = await parseFile(file, this.options);
210210+ const parseResult = await parseFile(file, this.options.parse);
245211 $refAdded.value = parseResult.result;
246212 schema = parseResult.result;
247247- } catch (err) {
248248- if (isHandledError(err)) {
249249- $refAdded.value = err;
213213+ } catch (error) {
214214+ if (isHandledError(error)) {
215215+ $refAdded.value = error;
250216 }
251251-252252- throw err;
217217+ throw error;
253218 }
254219 }
255220···306271 fetch,
307272 file,
308273 });
309309- const parseResult = await parseFile(file, this.options);
274274+ const parseResult = await parseFile(file, this.options.parse);
310275 $refAdded.value = parseResult.result;
311276 schema = parseResult.result;
312312- } catch (err) {
313313- if (isHandledError(err)) {
314314- $refAdded.value = err;
277277+ } catch (error) {
278278+ if (isHandledError(error)) {
279279+ $refAdded.value = error;
315280 }
316316-317317- throw err;
281281+ throw error;
318282 }
319283 }
320284
+5-10
packages/json-schema-ref-parser/src/parse.ts
···3131/**
3232 * Parses the given file's contents, using the configured parser plugins.
3333 */
3434-export const parseFile = async (
3434+export async function parseFile(
3535 file: FileInfo,
3636- options: $RefParserOptions,
3737-): Promise<PluginResult> => {
3636+ options: $RefParserOptions['parse'],
3737+): Promise<PluginResult> {
3838 try {
3939 // If none of the parsers are a match for this file, try all of them. This
4040 // handles situations where the file is a supported type, just with an
4141 // unknown extension.
4242- const parsers = [
4343- options.parse.json,
4444- options.parse.yaml,
4545- options.parse.text,
4646- options.parse.binary,
4747- ];
4242+ const parsers = [options.json, options.yaml, options.text, options.binary];
4843 const filtered = parsers.filter((plugin) => plugin.canHandle(file));
4944 return await plugins.run(filtered.length ? filtered : parsers, file);
5045 } catch (error: any) {
···62576358 throw new ParserError(error.error.message, file.url);
6459 }
6565-};
6060+}
+5
packages/json-schema-ref-parser/src/refs.ts
···152152 throw ono(`Error resolving $ref pointer "${path}". \n"${withoutHash}" not found.`);
153153 }
154154155155+ if ($ref.value === undefined) {
156156+ console.warn(`$ref entry exists but value is undefined: ${withoutHash}`);
157157+ return null; // Treat as unresolved
158158+ }
159159+155160 return $ref.resolve(absPath, options, path, pathFromRoot);
156161 }
157162