Circular References in OpenAPI (v0.12)
How libopenapi detects and warns about them (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 circular references
Circular references are loops in a data structure or Schema
that causes loops.
components:
schemas:
One:
properties:
thing:
"$ref": "#/components/schemas/Two"
required:
- thing
Two:
description: "test two"
properties:
testThing:
"$ref": "#/components/schemas/One"
This creates a circular reference. One
depends Two
which depends on One
One –> Two –> One –>… and so on.
The effect of ‘required’
A circular reference isn’t considered something that’s broken, unless required
is set to true. When a schema
is required AND it’s part of a loop - an unrecoverable infinite loop.
Detecting circular references
Internally, the resolver looks at every single reference found in the document, by using the index to look them all up, and then it follows the dependency graph for each and every other reference found.
There are opportunities to use references everywhere, and in Schemas, it can get quite messy. Polymorphic dependencies can create all kinds of chaos at scale.
libopenapi
will find them all, and be able to detect if they are infinte loops.
There are two ways to detect circular references programmatically:
Automatically with a model
When building a data model, A resolver instance
is created during the build, and runs the CheckForCircularReferences()
.
CheckForCircularReferences()
is a non-destructive operation on the resolver that follows
the same code-path as the resolver (so it can detect all the circles), however it doesn’t actually resolve anything
or change the tree in anyway.If any infinite loops were detected, they are collected up and returned as a slice of resolver.ResolvingError
and returned by the second argument of the BuildVXModel()
.
It’s easy to extract details about the circular reference from the ResolvingError
because, there will be a pointer to
a index.CircularReferenceResult
as a part
of the error, available via the CircularReference
property of the ResolvingError
struct.
For example:
import (
"fmt"
"github.com/pb33f/libopenapi"
"github.com/pb33f/libopenapi/resolver"
"io/ioutil"
)
func main() {
// load an OpenAPI 3 specification with circular refs from bytes
circularBytes, _ := ioutil.ReadFile("test_specs/circular_tests.yaml")
// create a new document from specification bytes
document, _ := libopenapi.NewDocument(circularBytes)
// build a v3 model from the document.
v3Model, errors := document.BuildV3Model()
// print out resolving errors:
// a slice of errors will be returned
for i := range errors {
// check if this is a resolving error.
if err, ok := errors[i].(resolver.ResolvingError); ok {
// check if there is a circular reference attached.
if err.CircularReference != nil {
fmt.Printf("Circular reference found: %s\n",
err.CircularReference.GeneerateJourneyPath())
}
}
}
Using the resolver
If your application’s use case doesn’t require a model, you can perform the check independently using the resolver (this is what the model builder does under the hood)
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 with circular refs from bytes
circularBytes, _ := ioutil.ReadFile("test_specs/circular_tests.yaml")
// create a new root node *yaml.Node reference
var rootNode yaml.Node
// unmarshal the specification bytes into the root node
_ = yaml.Unmarshal(circularBytes, &rootNode)
// create and index from the specification root node
idx := index.NewSpecIndex(&rootNode)
// create a new resolver from the index
resolverRef := resolver.NewResolver(idx)
// get circular reference errors (without resolving)
circularErrors := resolverRef.CheckForCircularReferences()
// iterate through circular errors extacted from resolver.
for _, err := range circularErrors {
fmt.Printf("Error: %s\n", err.Error())
}
}
Circular reference results
When a circular reference is detected, a index.CircularReferenceResult
is created that contains all the information about the circular reference, including the path taken to get there.
// CircularReferenceResult contains a circular reference found when traversing the graph.
type CircularReferenceResult struct {
Journey []*Reference
Start *Reference
LoopIndex int
LoopPoint *Reference
IsArrayResult bool // if this result comes from an array loop.
PolymorphicType string // which type of polymorphic loop is this? (oneOf, anyOf, allOf)
IsPolymorphicResult bool // if this result comes from a polymorphic loop.
IsInfiniteLoop bool // if all the definitions in the reference loop are marked as required
}
When a polymorphic or array based circular reference is detected, the IsPolymorphicResult
or IsArrayResult
properties
are set. This is useful for ignoring these types of circular references, as they may be valid use cases for a particular
specification design.
The PolymorphicType
property is set when the IsPolymorphicResult
is set to true
, and will be set to either oneOf
,
anyOf
or allOf
depending on the type of polymorphic loop detected.
CheckForCircularReferences()
is non-destructive and won’t change the node tree in any way.Polymorphic circular references
Using oneOf
, anyOf
or allOf
can create circular references, and they are detected in the same way as any other loop,
however these may actually be valid use cases for a particular specification design. In some cases, it may be desirable to
loop in common properties.
Here is an example of a polymorphic circular reference:
components:
schemas:
One:
properties:
thing:
oneOf:
- "$ref": "#/components/schemas/Two"
- "$ref": "#/components/schemas/Three"
required:
- thing
Two:
description: "test two"
properties:
testThing:
"$ref": "#/components/schemas/One"
Three:
description: "test three"
properties:
testThing:
"$ref": "#/components/schemas/One"
This creates a circular reference. One
depends Two
which depends on One
–> Two
–> One
–>… and so on.
Ignore polymorphic references
To stop libopenapi
from complaining about circles found through polymorphic references, you can tell the resolver to
simply ignore them by using the IgnorePolymorphicCircularReferences()
method available on the resolver.
newResolver := resolver.NewResolver(idx)
newResolver.IgnoreArrayCircularReferences()
If you’re building an OpenAPI document, there is a configuration property named IgnorePolymorphicCircularReferences
on
the datamodel.DocucmentConfiguration
struct to ignore polymorphic circular references when building a model at a high level.
var spec = `openapi: 3.1.0
...`
docConfig := datamodel.NewClosedDocumentConfiguration()
// ignore polymorphic circular references
docConfig.IgnorePolymorphicCircularReferences = true
document, err := NewDocumentWithConfiguration([]byte(spec), config)
...
Array based circular references
It’s common to see circular references in arrays, and they are detected in the same way as any other loop, however these may actually be valid use cases for a particular specification design. In some cases, it may be desirable to loop in common properties.
Here is an example of an array based circular reference
components:
schemas:
ProductCategory:
type: "object"
properties:
name:
type: "string"
children:
type: "array"
items:
$ref: "#/components/schemas/ProductCategory"
description: "Array of sub-categories in the same format."
required:
- "name"
- "children"
Ignore array references
To stop libopenapi
from complaining about circles found through polymorphic references, you can tell the resolver to
simply ignore them by using the IgnorePolymorphicCircularReferences()
method available on the resolver.
newResolver := resolver.NewResolver(idx)
newResolver.IgnoreArrayCircularReferences()
If you’re building an OpenAPI document, there is a configuration property named IgnoreArrayCircularReferences
on
the datamodel.DocucmentConfiguration
struct to ignore array based circular references when building a model at a high level.
var spec = `openapi: 3.1.0
...`
docConfig := datamodel.NewClosedDocumentConfiguration()
// ignore array circular references
docConfig.IgnoreArrayCircularReferences = true
document, err := NewDocumentWithConfiguration([]byte(spec), config)
...