Updated Jun 24th 2024: The API for iterating over maps in
libopenapi
was changed inv0.14
to use ordered maps, instead of regular maps. This change was made to ensure that the order of items in the map is preserved when rendering back out the OpenAPI specification. A few folks reported issues with this blog here and here. So I updated the code in this blog to reflect those changes.
If you’re using go as your application programming language of choice and want to parse an OpenAPI specification, this guide will help make this a breeze.
The library we’re going to use in this tutorial is libopenapi. It’s an enterprise-grade, high-performance library for OpenAPI, written in pure go. It supports OpenAPI 3.1 and 3.0 and Swagger (2.0)
I’m the author of libopenapi. It’s the same library that powers vacuum.
1. Install libopenapi in your project
Download the Petstore OpenAPI 3 sample specification that we’re going to be using in this tutorial.
The next step is to install libopenapi in your go application.
2. Read in the petstore OpenAPI specification
Read the petstore OpenAPI spec into a byte slice.
import (
"fmt"
"github.com/pb33f/libopenapi"
"os"
)
func main() {
// load in the petstore sample OpenAPI specification
// into a byte slice.
petstore, _ := os.ReadFile("petstorev3.json")
// create a new Document from from the byte slice.
document, err := libopenapi.NewDocument(petstore)
// if anything went wrong, an error is thrown
if err != nil {
panic(fmt.Sprintf("cannot create new document: %e", err))
}
Now we can build an OpenAPI 3 Model from the document.
// because we know this is a v3 spec, we can build a ready to go model from it.
docModel, errors := document.BuildV3Model()
If anything goes wrong, print out the errors, then panic.
// if anything went wrong when building the v3 model, a slice of errors will be returned
if len(errors) > 0 {
for i := range errors {
fmt.Printf("error: %e\n", errors[i])
}
panic(fmt.Sprintf("cannot create v3 model from document: %d errors reported", len(errors)))
}
That’s it! The model is now ready, and we can access it via docModel.Model
property.
Let’s explore a little by printing out a list of paths and how many operations they each have.
libopenapi
uses ordered maps (https://github.com/pb33f/libopenapi/blob/main/orderedmap/orderedmap.go)
this means we retain the order of the keys when re-rendering the model back out to yaml. This was added to the
library in v0.14
and permanently changed the API for all maps in the library.
for pathPairs := docmodel.Model.Paths.PathItems.First(); pathPairs != nil; pathPairs = pathPairs.Next() {
pathName := pathPairs.Key()
pathItem := pathPairs.Value()
fmt.Printf("Path %s has %d operations\n", pathName, pathItem.GetOperations().Len())
}
This will print out something like:
Path /store/order/{orderId} has 2 operations
Path /pet/findByStatus has 1 operations
Path /pet/findByTags has 1 operations
Path /pet has 2 operations
Path /pet/{petId}/uploadImage has 1 operations
Path /user/login has 1 operations
Path /user has 1 operations
...
To explore a little further, we can list out all the Schema
definitions found and how many properties they contain.
Each
Schema
reference is encapsulated by aSchemaProxy
, which means in order to generate a schema, you need to callSchema()
on theSchemaProxy
object. This prevents run-away circular references from exploding the stack.
for schemaPairs := model.Model.Components.Schemas.First(); schemaPairs != nil; schemaPairs = schemaPairs.Next() {
// get the name of the schema
schemaName := schemaPairs.Key()
// get the schema object from the map
schemaValue := schemaPairs.Value()
// build the schema
schema := schemaValue.Schema()
// if the schema has properties, print the number of properties
if schema != nil && schema.Properties != nil {
fmt.Printf("Schema '%s' has %d properties\n", schemaName, schema.Properties.Len())
}
}
Which will print out something like:
Schema 'Pet' has 3 properties
Schema 'Error' has 2 properties
Dealing with circular errors and resolving issues.
Schemas in OpenAPI are complex, and each version has a different set of supported properties.
One of the design attractions of OpenAPI is the ability to use references to prevent duplicating the same code over and over for reusable schemas and other object types.
libopenapi will automatically resolve JSON Schema references when building a model. Some specs use recursive/circular patterns when designing models (which is supported) but can cause severe problems for some tools.
When libopenapi reads the specification into a model, it automatically resolves all of those references and will detect anything circular.
A circular reference means the reference loops back around on itself as part of a chain that runs through direct or polymorphic sub-schemas.
For some specs, this may be by design (like the Stripe OpenAPI specification). However, for others, it may not be desirable.
If any circular/resolving errors are available when reading the OpenAPI specification,
libopenapi returns them with the BuildV3Model()
method.
The errors won’t prevent the model from building, you can choose to ignore them if you want.
1. Download the Stripe OpenAPI Specification.
Like earlier, read the bytes and create a new Document using libopenapi.NewDocument().
// load Stripe OpenAPI specification into
stripe, _ := os.ReadFile("stripe.json")
// create a new document from Stripe specification bytes
document, err := libopenapi.NewDocument(stripe)
// if anything went wrong, an error is thrown
if err != nil {
panic(fmt.Sprintf("cannot create new document: %e", err))
}
This spec has several circular errors. Instead of reading and using the model,
we can use errors and cast them *resolver.ResolvingError
.
Let’s print out some details from the circular references in the Stripe API.
// build a model from the Stripe document, ignore the model, keep the errors
_, errors := document.BuildV3Model()
// loop through errors and print out interesting details about them.
if len(errors) > 0 {
for i := range errors {
if circError, ok := errors[i].(*resolver.ResolvingError); ok {
fmt.Printf("Message: %s\n--> Loop starts line %d | Polymorphic? %v\n\n",
circError.Error(),
circError.CircularReference.LoopPoint.Node.Line,
circError.CircularReference.IsPolymorphicResult)
}
}
}
Which will print something like this:
Message: Circular reference detected: file: person -> legal_entity_person_verification -> legal_entity_person_verification_document -> file -> file_link -> file [8893:15]
--> Loop starts line 8893 | Polymorphic? true
Message: Circular reference detected: bank_account: payment_intent -> customer -> bank_account -> account -> bank_account [2291:23]
--> Loop starts line 2291 | Polymorphic? true
...
What about parsing Swagger specifications?
If Swagger (OpenAPI 2) is something you’re still using, I recommend upgrading to OpenAPI version. 3.1
However, if that’s something that you can’t do, then libopenapi has got you covered.
The process is the same as building an OpenAPI 3+ document; however, call BuildV2Model()
instead.
// for a v2 (Swagger) specification, build a v2 model
swaggerDocModel, errors := document.BuildV2Model()
libopenapi will render a different model. Swagger differs in many ways from OpenAPI, so most of the model is not shared.
Read the full docs for libopenapi at pb33f.io
Read the go docs for libopenapi at pkg.go.dev