Resolving OpenAPI specification references (v0.12)
The resolver stitches references together (v0.12).[Version 0.12 and below only]
Since v0.13 the index and resolver architecture has changed significantly, these documents are for v0.12 and below.
View the latest documentation for the resolver
If you have ever used OpenAPI with golang, you’re probably aware of using $ref
to reference
other objects like Schemas or Parameters that we want to re-use across a specification, or across multiple specifications.
liboenapi
knows how to look up these references and resolve them.
What does resolving mean?
A $ref
is a JSON Pointer that points to a location in the specification. The
location could be local to the current document, or it could be in another document on the local file system, or out
there in cyberspace somewhere.
Resolving is a two-step process, the first being locating the document (local, file or remote) and then finding the object being pointed to inside that document.
The second process is to then create a new document but with the $ref
instances, replaced with the
actual object referenced.
How libopenapi resolves references
First of all we create an index of the specification that provides a lookup for everything in the OpenAPI specification.
Local references
The index will locate every $ref
and determine what type of lookup to perform, if the
reference is local, there is no prefix attached to the reference path, so it looks something like this:
$ref: "#/components/schemas/Burger"
The # (hash) symbol indicates the root of the document.
The index looks up the component from within the same document and keeps a record of the found object.
File references
If the reference is a file reference, it will look something like this:
$ref: "some/path/to/openapi-schemas.yaml#/components/schemas/Burger"
The prefix (before the hash) is the path to the file that contains the lookup. libopenapi
will attempt to locate the file
on the local operating system and then load it into the index. If the path is relative, the resolver will look from where
it is being run.
When a file reference is used, a whole new index is created for the entire file, so the index, maintains indexes for every file pulled in.
Remote references
If the reference is a remote reference, it will look something like this:
$ref: "https://pb33f.io/openapi.yaml#/components/schemas/Burger"
Like file references, the prefix (before the hash) is the URI to the file that contains the lookup. libopenapi
will
pull down this remote document and then treat it as a local reference internally.
When a remote reference is used, a whole new index is created for the entire remote file (like file references).
learn more about remote/relative references
Sticking it all together
So far, we have only talked about the index and how it locates references. The resolver is actually responsible for looking at every reference in the document, and then locating the object it references from the index(es) and then replacing the reference with the actual object.
This is not useful_ for humans, but it is useful for machines, because it means that the document is now self-contained.
The resolver exists because it (alongside libopenapi
) was born inside vacuum.
vacuum does not use the data model to operate, it works on the low level *yaml.Node structure and needs a fully resolved node tree to work correctly.
The document is now much, much bigger than it was before and there is duplication everywhere, but
the document now exists in a state, as if $ref
and JSON Pointers did not exist.
We don’t recommend saving or rendering a resolved spec, it’s probably going to be gigantic.
Why do I need a resolver?
If we’re just using the data model to work with OpenAPI specifications, we don’t need to resolve specifications.
However, if our application has a use-case like vacuum in which the application needs to recurse through an entire OpenAPI specification and treat everything as in-line, for the sake of validation - then a resolver is what’s need.
How do I use the resolver?
In order to resolve a spec, first we need an index. The index is used by the resolver to locate local, file and remote references.
import (
"fmt"
"io/ioutil"
"gopkg.in/yaml.v3"
"github.com/pb33f/libopenapi/index"
"github.com/pb33f/libopenapi/resolver"
)
func main {
// load an OpenAPI 3 specification from bytes
petstore, _ := ioutil.ReadFile("petstorev3.json")
// create a new root node *yaml.Node reference
var rootNode yaml.Node
// unmarshal the specification bytes into the root node
_ = yaml.Unmarshal(petstore, &rootNode)
// create and index from the specification root node
idx := index.NewSpecIndex(&rootNode)
// create a new resolver from the index
resolverRef := resolver.NewResolver(idx)
// resolve the openapi specifications
resolvingErrors := resolverRef.Resolve()
// any errors found during resolution? Print them out.
for _, err := range resolvingErrors {
fmt.Printf("Error: %s\n", err.Error())
}
}
Error types
When resolving, anything that goes wrong along the way is recorded as a resolver.ResolvingError
and returned as a slice of errors.
Anything other than a circular reference should be considered something that went wrong during resolution. The most common error is missing local or remote references, or un-locatable ones.
Circular references are complex, they are not errors in a traditional sence, but they are something that needs to be handled (or ignored) by the application.