DocumentsHey API
JSON Schema Ref Parser
JSON Schema Ref Parser
Type
Topic
Status
Published
Created
Jun 17, 2026
Updated
Jun 17, 2026
Created by
Dosu Bot
Updated by
Dosu Bot

JSON Schema $Ref Parser#

@hey-api/json-schema-ref-parser is hey-api's fork of the community json-schema-ref-parser library, maintained inside the hey-api/openapi-ts monorepo . It is a first-party dependency of @hey-api/openapi-tsimported as $RefParser and ResolverError in createClient.ts — and is not intended as a standalone public utility.

The package solves the standard JSON Reference problem: an OpenAPI spec may spread its definitions across many local files and remote URLs, using $ref pointers in mixed JSON/YAML format. The library crawls that graph, resolves every external pointer, and returns a single, fully-connected JavaScript object ready for code generation.

Key additions over the upstream fork (README):

  • bundleMany — bundles multiple root schemas (e.g., multiple API spec files) into one merged root
  • excludedPathMatcher(path) => boolean — allows callers to skip specific external references during resolution
  • onDereference(path, value, parent, parentPropName) callback hook
  • Improved $id-aware $ref resolution

The primary entry point is src/index.ts.

How It Fits in the openapi-ts Pipeline#

$RefParser is instantiated once per generation job in createClient.ts. The flow is:

  1. Fetch — raw spec bytes (ArrayBuffer) are fetched per configured input via getSpec().
  2. Parse / bundle — the parser is called:
  3. PatchpatchOpenApiSpec() mutates the bundled object using any parser.patch config the user provided .
  4. Parse IRparseOpenApiSpec(context) converts the resolved object into hey-api's internal representation for code generation.

The parser configuration exposed to users via openapi-ts.config.ts (the parser: key) operates above the ref-parser layer — the patch, filters, transforms, and hooks options documented on heyapi.dev/docs/openapi/typescript/configuration/parser are applied after $RefParser has already flattened all $ref pointers into a single object.

Schema Bundling and bundleMany#

bundle() handles a single root schema. It parses the file, resolves all external refs, runs the bundler, and checks for accumulated errors. The result is a schema where all external $ref pointers have been rewritten to internal #/... pointers — safe to serialize with JSON.stringify().

bundleMany() is the key addition for multi-spec workflows . Its internal steps:

  1. parseMany() — reads each input independently and collects them in this.schemaMany .
  2. mergeMany() — builds a single synthetic merged root by :
    • Deriving a per-source prefix from the source file/URL basename (e.g., petstore for petstore.yaml)
    • Prefixing all component names (schemas, parameters, requestBodies, etc.) with that prefix to avoid collisions
    • De-duplicating servers by URL+description
    • Merging paths, falling back to /{prefix}/{path} on HTTP-method conflicts
    • Rewriting all internal $ref pointers to point at the renamed components
  3. Then follows the same resolveExternalbundle → error-check pipeline as the single-schema path.

The sourcePathToPrefix map stored on the parser instance tracks which prefix was assigned to each source path, allowing downstream consumers to trace generated symbols back to their origin spec .

External $ref Resolution#

resolveExternal() is called on the root schema before bundling. It:

  • Recursively crawls the schema object looking for nodes where $Ref.isExternal$Ref(obj) is true .
  • For each external ref, fetches the target (file via fileResolver, URL via urlResolver), parses it, and stores the result in parser.$refs .
  • Deduplicates: if a ref path is already in $refs._$refs, the file is not re-fetched — the cached value is crawled for its own nested externals instead .
  • All external resolution happens concurrently via Promise.all() .

Only external $refs are resolved at this stage. Internal refs (starting with #) are left for the bundler to rewrite.

Error Handling#

The package exports a rich error hierarchy from src/util/errors.ts:

ClassCodeMeaning
JSONParserErrorEUNKNOWNBase class
ParserErrorEPARSERFile could not be parsed (bad JSON/YAML)
UnmatchedParserErrorEUNMATCHEDPARSERNo parser found for the file type
ResolverErrorERESOLVERFile/URL could not be read; carries ioErrorCode (e.g. ENOENT)
UnmatchedResolverErrorEUNMATCHEDRESOLVERNo resolver matched the scheme
MissingPointerErrorEMISSINGPOINTER$ref token does not exist in the target
TimeoutErrorETIMEOUTDereferencing took longer than the configured timeout
InvalidPointerErrorEUNMATCHEDRESOLVERPointer does not start with #/

Multiple errors from a single parse run are collected into a JSONParserErrorGroup, which aggregates all per-$ref errors .

In createClient.ts, ResolverError with ioErrorCode === 'ENOENT' is specifically caught and re-thrown as a user-friendly InputError('Input file not found') . All other errors propagate up as-is, ultimately reaching the CLI error handler.

Schema Hoisting in the Bundler#

src/bundle.ts implements the hoisting logic that promotes external schema definitions into the root document's #/components/... namespace. For each $ref node it encounters, inventory$Ref() records:

  • whether the ref is external or circular
  • the depth and indirection count
  • the originalContainerType — inferred from the JSON Pointer path by getContainerTypeFromPath(), which maps path tokens (parameters, requestBody, headers, responses, schema) to the correct OpenAPI component bucket.

The bundler uses a Map-backed inventoryLookup (rather than a linear scan with deep equality) for O(1) deduplication of already-inventoried refs . After the full inventory is built, external $ref values are hoisted to #/components/{containerType}/{name} and all pointers are rewritten to the new internal paths.

JSON Schema Ref Parser | Dosu