@apiture/api-ref-resolver
api-ref-resolver
resolves multi-file API definition documents by replacing
external {$ref: "uri"}
JSON Reference
objects with the object referenced at the uri
.
The uri
may be a file-path or a URL with an optional
#
JSON Pointer fragment.
For example, if components.yaml
contains:
paths:
'/health':
get:
operationId: apiHealth
description: Return API Health
tags:
- Health
responses:
'200':
description: OK. The API is alive and active.
content:
application/json:
schema:
$ref: '#/components/schemas/health'
components:
parameters:
idempotencyKeyHeaderParam:
name: Idempotency-Key
description: Idempotency Key to guarantee client requests and not processed multiple times.
in: header
schema:
type: string
schemas:
health:
title: API Health
description: API Health response
type: object
properties:
status:
description: The API status.
type: string
enum:
- pass
- fail
- warn
and api.yaml
contains
paths:
/health:
get:
$ref: 'components.yaml#/paths/~1health/get'
/thing:
parameters:
- $ref: 'components.yaml#/components/parameters/idempotencyKeyHeaderParam'
then running
api-ref-resolver -i api.yaml -o resolved-api.yaml
will yield the following in resolved-api.yaml
:
paths:
/health:
get:
operationId: apiHealth
description: Return API Health
tags:
- Health
responses:
'200':
description: OK. The API is alive and active.
content:
application/json:
schema:
$ref: '#/components/schemas/health'
x-resolved-from: >-
components.yaml#/paths/~1health/get
/thing:
parameters:
- $ref: '#/components/parameters/idempotencyKeyHeaderParam'
components:
parameters:
idempotencyKeyHeaderParam:
name: Idempotency-Key
description: >-
Idempotency Key to guarantee client requests and not processed multiple
times.
in: header
schema:
type: string
x-resolved-from: >-
components.yaml#/components/parameters/idempotencyKeyHeaderParam
schemas:
health:
title: API Health
description: API Health response
type: object
properties:
status:
description: The API status.
type: string
enum:
- pass
- fail
- warn
x-resolved-from: >-
components.yaml#/components/schemas/health
x-resolved-from: >-
api.yaml
x-resolved-at: '2022-03-11T16:27:59.365Z'
The tool handles chains of JSON references (i.e. a.yaml
references components from b.yaml
which references components from c.yaml
) as
well as direct or indirect cycles (component A
references component B
which references component A
).
Unlike other generic $ref
resolvers (1, 2, 3),
api-ref-resolver
treats components
references specially.
It understands reusable components/section/componentName
objects at the top-level of an API definition, such as #/components/schemas/schemaName
, and attempts to
maintain those component structures; see Notes below.
Otherwise, it is specification agnostic and works with either
OpenAPI specification or AsyncAPI specification.
This tool does not enforce JSON Reference strictness; that is, the $ref
member may have siblings, as used in OpenAPI 3.1 Reference Objects.
Use
Command Line Interface
api-ref-resolver --input api.yaml --output resolved-api.yaml
arr --input api.yaml --output resolved-api.yaml
arr -i api.yaml | some-other-pipeline >| resolved-api.yaml
Command line options:
Usage: api-ref-resolver [options]
Options:
-V, --version output the version number
-i, --input <input-file> An openapi.yaml or asyncapi.yaml file name or URL. Defaults to "api.yaml"
-n, --no-markers Do not add x-resolved-from and x-resolved-at markers
-o, --output <output-file> The output file, defaults to stdout if omitted
-f, --format [yaml|json] Output format for stdout if no --output option is used; default to yaml
-v, --verbose Verbose output
-h, --help display help for command
Node.js
import { ApiRefResolver } from '@apiture/api-ref-resolver';
import * as fs from 'fs';
import * as yaml from 'js-yaml';
const sourceFileName = 'api.yaml'
const outputFileName = 'resolved-api.yaml'
const resolver = new ApiRefResolver(sourceFileName);
const options: ApiRefOptions = {
verbose: false,
conflictStrategy: 'error',
outputFormat: 'yaml'
};
options.verbose = opts.verbose;
resolver
.resolve(options)
.then((resolved) => {
fs.writeFileSync(outputFileName, yaml.dump(resolved.api), 'utf8');
})
.catch((ex) => {
console.error(ex.message);
process.exit(1);
});
or with async
/await
:
try {
const resolved = await resolve(options);
fs.writeFileSync(outputFileName, yaml.dump(resolved.api), 'utf8');
} catch (e) {
}
Notes
Below, a normalized path is defined as the simplified
version of a file-path or URL, i.e. with ../
path elements collapsed.
The normalized path for ../a/b/c/../../d/e
is ../a/d/e
.
Local references that begin with #
, such as { $ref: "#/path/to/element" }
,
are left as-is.
There are three types of replacements:
Component Replacements,
Full resource replacements,
and Other embedded objects.
Component replacements
Component replacements are of the form
{ $ref: "uri#/components/section/componentName" }
(section
may be schemas
,
parameters
, response
, or any other item in components
).
Component replacements are only done for three-level JSON Pointers; for longer JSON pointers, see #4 below.
If the containing $ref object is at /components/section/componentName0
, it does not contain any other keys, and
componentName0
equals componentName
, the entire referenced object is inserted
in place of the original $ref
object and the mapping uri#/components/section/componentName
⇒ #/components/section/componentName
is remembered.
This is useful to reuse security schemes in OpenAPI 3.1, which are reference by names instead of a $ref
.
For example, if common.yaml
contains the definition of the apiKey
security schema:
components:
securitySchemes:
apiKey:
type: apiKey
name: API-Key
in: header
description: 'API Key based client identification.'
then other API source files can reference this via
paths:
'/some/path':
get:
security:
apiKey: []
components:
securitySchemes:
apiKey:
$ref: '../common.yaml#/components/securitySchemes/apiKey'
This tool will replace the $ref
definition of apiKey
with the one from common.yaml
:
paths:
'/some/path':
get:
security:
apiKey: []
components:
securitySchemes:
type: apiKey
name: API-Key
in: header
description: 'API Key based client identification.'
x-resolved-from: common.yaml#/components/securitySchemes/apiKey
In a more complicated case (where the $ref
contains other properties,
preventing a simple replacement),
the content at the external URI is read and the new named component is
inserted into the target document's components object. The non-local $ref
( ../common.yaml#/components/responses/404
in this case) replaced by
a local ref, such as { $ref: "#/components/responses/404" }
.
For example, if an API has several operations that can return a 404 when a thing
is not found, it may define the reusable component response
with a clean description of the problem:
paths:
/thing/{thingId}:
get:
...
responses:
'404':
$ref: '#/components/responses/404Thing'
put:
...
responses:
'404':
$ref: '#/components/responses/404Thing'
patch:
...
responses:
'404':
$ref: '#/components/responses/404Thing'
components:
responses:
'404Thing':
description: Thing not found at /thing/{thingId}.
$ref: 'common.yaml#/components/responses/404'
The tool will inline the 404
response from common.yaml
as a component,
then replace the remote $ref
inside thr 404Thing
response with a reference to the local, inlined 404
:
components:
responses:
'404':
description: Not found. There is no such resource at the request URL.
content:
application/json:
schema:
$ref: '#/components/schemas/problemResponse'
x-resolved-from: common.yaml#/components/responses/404
404Thing:
description: Thing not found at /thing/{thingId}.
$ref: '#/components/responses/404'
The ApiRefOptions.conflictPolicy
determines what to do if the componentName
already exists in the target document:
- it is either renamed with a unique numeric suffix (
rename
); - it is an error and the entire process fails (
error
) - the conflict is ignored (
ignore
).
Note: The OpenAPI Specification requires that these paths be relative to the
path in the
servers
object, but this tool simply uses relative references
from the source URI.
Full resource replacements
Full resource replacements are of the form
{ $ref: "uri" }
with no #
fragment. If not yet seen, the entire external file
is inserted, replacing the $ref
object. The location is
remembered so that any duplicate references to the normalized
path are replaced with a local { $ref: #/location/of/resolved/resource }
.
This is only done if the $ref
is the only key in the object.
Other embedded objects
When referencing non-component objects, such as
{ $ref: "components.yaml#/paths/~1health/get" }
to include the get
operation at
the OpenAPI path /health
the operation object in components.yaml
.
After embedding an external object from uri
, the tool will also rewrite any
$ref
objects within it, relative to the path that the object was read from.
Any { $ref: "#/..."}
objects are converted to { $ref: "normalized-path#/..."}
.
To Do
This tool does not yet merge non-$ref
content from API files. For example, if
one file has a $ref
to an operation in another file, this tool
does not pull in API elements from the referenced file, such as the
tags
and security
requirements of the referenced operation.