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.

The index is actually responsible for ‘pulling’ in all the referenced documents and creating a single, unified, in-memory representation of 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.

Make sure the files exist locally before attempting to use relative references.

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())
    }
}
Resolving is a destructive operation, meaning that the node tree is permanently changed, references will be permanently shifted in that model.

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.