OpenAPI is simple and easy to parse using go. I'll show you how in five minutes.
OpenAPI is simple and easy to parse using go. I’ll show you how in five minutes.

Updated Jun 24th 2024: The API for iterating over maps in libopenapi was changed in v0.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.

curl https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/examples/v3.0/petstore.yaml > petstorev3.json

The next step is to install libopenapi in your go application.

go get github.com/pb33f/libopenapi

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 a SchemaProxy, which means in order to generate a schema, you need to call Schema() on the SchemaProxy 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.

curl https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json > stripe.json

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