Handling extensions using libopenapi

Using OpenAPI vendor extensions, both simple and complex

OpenAPI extensions (also known as vendor extensions) are properties with a prefix of x- that can be added to many OpenAPI objects throughout the OpenAPI specification.

Both high-level and low-level models support extensions and are available on both via the Extensions property available on all models that support it.

Extensions can be anything! This means there could be a scalar value like a string, or it could be a highly complex, deeply nested object. When we don’t know what’s in the can, its safest to not assume anything.

High level models

It’s pretty simple, all high-level models have the signature:

type SomeHighLevelOpenAPIObject struct {
 
  ...
  Extensions map[string]any // <-- all compatible high level
                            //     objects have this extension 
                            //     signature.
 
 }

Low level models

All low-level models have the signature:

type SomeLowLevelOpenAPIObject struct {
 
  ...
  // all compatible low-level objects have this extension signature.
  Extensions map[low.KeyReference[string]]low.ValueReference[any] 
 
 }

Simple extensions

If the specification uses a simple/primitive extension like string or int then, just cast to that type and you’re on your way.

Complex Extensions

Let’s say the extension x-custom-cakes is an object, that looks something like this:

openapi: '3.1'
  components:
    schemas:
      MySchema:
        description: "Some schema with custom complex extensions"
        x-custom-cakes:
          description: some cakes
          cakes:
            someCake:
              candles: 10
              frosting: blue
              someStrangeVarName: mapping is required to extract these.
            anotherCake:
              candles: 1
              frosting: green

The object that represents x-custom-cakes looks like this:

// define an example struct representing a cake
// super important to remember to use hints/meta-data to map properties correctly.
type cake struct {
    Candles               int    `yaml:"candles"`
    Frosting              string `yaml:"frosting"`
    Some_Strange_Var_Name string `yaml:"someStrangeVarName"`
}

// define a struct that holds a map of cake pointers.
type cakes struct {
    Description string
    Cakes       map[string]*cake
}

When we’re reading the high-level models and we want to unpack our extensions into these complex structs from a type of map[string]any?

UnpackExtensions

There is a method to make this simple available in the high package within the datamodel package. It’s called UnpackExtensions

It’s a generic function that requires the custom type and the low-level model type of the object that contains the extensions you’d like to unpack.

Here is an example from our cake spec.


import (
    "github.com/pb33f/libopenapi"
    high "github.com/pb33f/libopenapi/datamodel/high"
    lowbase "github.com/pb33f/libopenapi/datamodel/low/base"
    lowv3 "github.com/pb33f/libopenapi/datamodel/low/v3"
)

type cake struct {
    Candles               int    `yaml:"candles"`
    Frosting              string `yaml:"frosting"`
    Some_Strange_Var_Name string `yaml:"someStrangeVarName"`
}

type cakes struct {
    Description string
    Cakes       map[string]*cake
}

func main() {

  // create a specification with a schema and parameter that use complex 
  // custom cakes and burgers extensions.
  spec := `
openapi: "3.1"
components:
  schemas:
    MySchema:
      description: "Some schema with custom complex extensions"
      x-custom-cakes:
        description: some cakes
        cakes:
          someCake:
            candles: 10
            frosting: blue
            someStrangeVarName: mapping is required to extract these.
          anotherCake:
            candles: 1
            frosting: green
  `
  
  // create a new document from specification bytes, ignore errors
  doc, _ := libopenapi.NewDocument([]byte(spec))
      
  // build a v3 model, ignore errors
  docModel, _ := doc.BuildV3Model()
   
  // get a reference to MySchema
  mySchema := docModel.Model.Components.Schemas["MySchema"].Schema()
  
  // unpack mySchema extensions into complex `cakes` type
  mySchemaExtensions, schemaUnpackErrors := high.UnpackExtensions[cakes, *lowbase.Schema](mySchema)
  if schemaUnpackErrors != nil {
      panic(fmt.Sprintf("cannot unpack schema extensions: %e", err))
  }
  
  // extract extension by name for mySchema
  customCakes := mySchemaExtensions["x-custom-cakes"]
      
  // print out mySchema complex extension details.
  fmt.Printf("mySchema 'x-custom-cakes' (%s) has %d cakes, " + 
             "'someCake' has %d candles and %s frosting\n",
              customCakes.Description,
              len(customCakes.Cakes),
              customCakes.Cakes["someCake"].Candles,
              customCakes.Cakes["someCake"].Frosting,
  )
}

This will output:

mySchema 'x-custom-cakes' (some cakes) has 2 cakes, 'someCake' has 10 candles and blue frosting