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-ts — imported 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 rootexcludedPathMatcher(path) => boolean— allows callers to skip specific external references during resolutiononDereference(path, value, parent, parentPropName)callback hook- Improved
$id-aware$refresolution
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:
- Fetch — raw spec bytes (
ArrayBuffer) are fetched per configured input viagetSpec(). - Parse / bundle — the parser is called:
- Single input →
refParser.bundle() - Multiple inputs →
refParser.bundleMany()
- Single input →
- Patch —
patchOpenApiSpec()mutates the bundled object using anyparser.patchconfig the user provided . - Parse IR —
parseOpenApiSpec(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:
parseMany()— reads each input independently and collects them inthis.schemaMany.mergeMany()— builds a single synthetic merged root by :- Deriving a per-source
prefixfrom the source file/URL basename (e.g.,petstoreforpetstore.yaml) - Prefixing all component names (
schemas,parameters,requestBodies, etc.) with that prefix to avoid collisions - De-duplicating
serversby URL+description - Merging
paths, falling back to/{prefix}/{path}on HTTP-method conflicts - Rewriting all internal
$refpointers to point at the renamed components
- Deriving a per-source
- Then follows the same
resolveExternal→bundle→ 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 viaurlResolver), parses it, and stores the result inparser.$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:
| Class | Code | Meaning |
|---|---|---|
JSONParserError | EUNKNOWN | Base class |
ParserError | EPARSER | File could not be parsed (bad JSON/YAML) |
UnmatchedParserError | EUNMATCHEDPARSER | No parser found for the file type |
ResolverError | ERESOLVER | File/URL could not be read; carries ioErrorCode (e.g. ENOENT) |
UnmatchedResolverError | EUNMATCHEDRESOLVER | No resolver matched the scheme |
MissingPointerError | EMISSINGPOINTER | $ref token does not exist in the target |
TimeoutError | ETIMEOUT | Dereferencing took longer than the configured timeout |
InvalidPointerError | EUNMATCHEDRESOLVER | Pointer 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 bygetContainerTypeFromPath(), 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.