libopenapi package - github.com/pb33f/libopenapi - Go Packages

Package libopenapi is a library containing tools for reading and in and manipulating Swagger (OpenAPI 2) and OpenAPI 3+ specifications into strongly typed documents. These documents have two APIs, a high level (porcelain) and a low level (plumbing).

Every single type has a 'GoLow()' method that drops down from the high API to the low API. Once in the low API, the entire original document data is available, including all comments, line and column numbers for keys and values.

There are two steps to creating a using Document. First, create a new Document using the NewDocument() method and pass in a specification []byte array that contains the OpenAPI Specification. It doesn't matter if YAML or JSON are used.

This section is empty.

This section is empty.

ClearAllCaches resets every global in-process cache in libopenapi. Call this between document lifecycles in long-running processes (servers, CLI tools that process many specs) to release memory that would otherwise accumulate and never be garbage-collected.

CompareDocuments will accept a left and right Document implementing struct, build a model for the correct version and then compare model documents for changes.

If there are any errors when building the models, those errors are returned with a nil pointer for the model.DocumentChanges. If there are any changes found however between either Document, then a pointer to model.DocumentChanges is returned containing every single change, broken down, model by model.

NewArazzoDocument parses raw bytes into a high-level Arazzo document.

NewOverlayDocument creates a new overlay document from the provided bytes. The overlay document can then be applied to a target OpenAPI document using ApplyOverlay.

Document Represents an OpenAPI specification that can then be rendered into a model or serialized back into a string document after being manipulated.

NewDocument will create a new OpenAPI instance from an OpenAPI specification []byte array. If anything goes wrong when parsing, reading or processing the OpenAPI specification, there will be no document returned, instead a slice of errors will be returned that explain everything that failed.

After creating a Document, the option to build a model becomes available, in either V2 or V3 flavors. The models are about 70% different between Swagger and OpenAPI 3, which is why two different models are available.

This function will NOT automatically follow (meaning load) any file or remote references that are found.

If this isn't the behavior you want, then you can use the NewDocumentWithConfiguration() function instead, which allows you to set a configuration that will allow you to control if file or remote references are allowed. In particular the `AllowFileReferences` and `FollowRemoteReferences` properties.

// How to read in an OpenAPI 3 Specification, into a Document.

// load an OpenAPI 3 specification from bytes
petstore, _ := os.ReadFile("test_specs/petstorev3.json")

// create a new document from specification bytes
document, err := NewDocument(petstore)
// if anything went wrong, an error is thrown
if err != nil {
	panic(fmt.Sprintf("cannot create new document: %e", err))
}

// because we know this is a v3 spec, we can build a ready to go model from it.
v3Model, err := document.BuildV3Model()

// if anything went wrong when building the v3 model, an error will be returned.
if err != nil {
	fmt.Printf("error: %e\n", err)
	panic(fmt.Sprintf("cannot create v3 model from document: %e", err))
}

// get a count of the number of paths and schemas.
paths := orderedmap.Len(v3Model.Model.Paths.PathItems)
schemas := orderedmap.Len(v3Model.Model.Components.Schemas)

// print the number of paths and schemas in the document
fmt.Printf("There are %d paths and %d schemas in the document", paths, schemas)
Output:

There are 13 paths and 8 schemas in the document
// How to read in a Swagger / OpenAPI 2 Specification, into a Document.

// load a Swagger specification from bytes
petstore, _ := os.ReadFile("test_specs/petstorev2.json")

// create a new document from specification bytes
document, err := NewDocument(petstore)
// if anything went wrong, an error is thrown
if err != nil {
	panic(fmt.Sprintf("cannot create new document: %e", err))
}

// because we know this is a v2 spec, we can build a ready to go model from it.
v2Model, err := document.BuildV2Model()

// if anything went wrong when building the v3 model, and error will be returned
if err != nil {
	fmt.Printf("error: %e\n", err)
	panic(fmt.Sprintf("cannot create v3 model from document: %e", err))
}

// get a count of the number of paths and schemas.
paths := orderedmap.Len(v2Model.Model.Paths.PathItems)
schemas := orderedmap.Len(v2Model.Model.Definitions.Definitions)

// print the number of paths and schemas in the document
fmt.Printf("There are %d paths and %d schemas in the document", paths, schemas)
Output:

There are 14 paths and 6 schemas in the document
// load an unknown version of an OpenAPI spec
burgershop, _ := os.ReadFile("test_specs/burgershop.openapi.yaml")

var paths, schemas int
var err error

// create a new document from specification bytes
document, err := NewDocument(burgershop)
// if anything went wrong, an error is thrown
if err != nil {
	panic(fmt.Sprintf("cannot create new document: %e", err))
}

// We don't know which type of document this is, so we can use the spec info to inform us
if document.GetSpecInfo().SpecType == utils.OpenApi3 {
	var v3Model *DocumentModel[v3high.Document]
	v3Model, err = document.BuildV3Model()
	if err == nil {
		paths = orderedmap.Len(v3Model.Model.Paths.PathItems)
		schemas = orderedmap.Len(v3Model.Model.Components.Schemas)
	}
}
if document.GetSpecInfo().SpecType == utils.OpenApi2 {
	var v2Model *DocumentModel[v2high.Swagger]
	v2Model, err = document.BuildV2Model()
	if err == nil {
		paths = orderedmap.Len(v2Model.Model.Paths.PathItems)
		schemas = orderedmap.Len(v2Model.Model.Definitions.Definitions)
	}
}

// if anything went wrong when building the model, report errors.
if err != nil {
	fmt.Printf("error: %e\n", err)
	panic(fmt.Sprintf("cannot create v3 model from document: %e", err))
}

// print the number of paths and schemas in the document
fmt.Printf("There are %d paths and %d schemas in the document", paths, schemas)
Output:

There are 5 paths and 6 schemas in the document
// This example shows how to create a document that prevents the loading of external references/
// from files or the network

// load in the Digital Ocean OpenAPI specification
digitalOcean, _ := os.ReadFile("test_specs/digitalocean.yaml")

// create a DocumentConfiguration that prevents loading file and remote references
config := datamodel.NewDocumentConfiguration()

// create a new structured logger to capture error logs that will be spewed out by the rolodex
// when it tries to load external references. We're going to create a byte buffer to capture the logs
// and then look at them after the document is built.
var logs []byte
buf := bytes.NewBuffer(logs)
logger := slog.New(slog.NewTextHandler(buf, &slog.HandlerOptions{
	Level: slog.LevelError,
}))
config.Logger = logger // set the config logger to our new logger.

// Do not set any baseURL, as this will allow the rolodex to resolve relative references.
// without a baseURL (for remote references, or a basePath for local references) the rolodex
// will consider the reference to be local, and will not attempt to load it from the network.

// create a new document from specification bytes
doc, err := NewDocumentWithConfiguration(digitalOcean, config)
// if anything went wrong, an error is thrown
if err != nil {
	panic(fmt.Sprintf("cannot create new document: %e", err))
}

// only errors will be thrown, so just capture them and print the number of errors.
_, err = doc.BuildV3Model()

// there should be 475 errors logs
logItems := strings.Split(buf.String(), "\n")
fmt.Printf("There are %d errors logged\n", len(logItems))

if err != nil {
	fmt.Println("Error building Digital Ocean spec errors reported")
}
Output:

There are 475 errors logged
Error building Digital Ocean spec errors reported
// load in the Digital Ocean OpenAPI specification
digitalOcean, _ := os.ReadFile("test_specs/digitalocean.yaml")

// Digital Ocean needs a baseURL to be set, so we can resolve relative references.
// baseURL, _ := url.Parse("https://raw.githubusercontent.com/digitalocean/openapi/main/specification")
// locked this in to a release, because the spec is throwing 404's occasionally.
baseURL, _ := url.Parse("https://raw.githubusercontent.com/digitalocean/openapi/ed0958267922794ec8cf540e19131a2d9664bfc7/specification")

// create a DocumentConfiguration that allows loading file and remote references, and sets the baseURL
// to somewhere that can resolve the relative references.
config := datamodel.DocumentConfiguration{
	BaseURL: baseURL,
	Logger: slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
		Level: slog.LevelError,
	})),
}

// create a new document from specification bytes
doc, err := NewDocumentWithConfiguration(digitalOcean, &config)
// if anything went wrong, an error is thrown
if err != nil {
	panic(fmt.Sprintf("cannot create new document: %e", err))
}

m, err := doc.BuildV3Model()

// if anything went wrong when building the v3 model, a slice of errors will be returned
if err != nil {
	fmt.Println("Error building Digital Ocean spec errors reported")
} else {
	fmt.Println("Digital Ocean spec built successfully")
}

// running this through a change detection, will render out the entire model and
// any stage two rendering for the model will be caught.
what_changed.CompareOpenAPIDocuments(m.Model.GoLow(), m.Model.GoLow())
Output:

Digital Ocean spec built successfully

If you want to know more about circular references that have been found during the parsing/indexing/building of a document, you can capture the []errors thrown which are pointers to *resolver.ResolvingError

// create a specification with an obvious and deliberate circular reference
spec := `openapi: "3.1"
components:
  schemas:
    One:
      description: "test one"
      properties:
        things:
          "$ref": "#/components/schemas/Two"
      required:
        - things
    Two:
      description: "test two"
      properties:
        testThing:
          "$ref": "#/components/schemas/One"
      required:
        - testThing
`
// create a new document from specification bytes
doc, err := NewDocument([]byte(spec))
// if anything went wrong, an error is thrown
if err != nil {
	panic(fmt.Sprintf("cannot create new document: %e", err))
}
_, errs := doc.BuildV3Model()

// extract resolving error
resolvingError := errs

// resolving error is a pointer to *resolver.ResolvingError
// which provides access to rich details about the error.

var circularReference *index.CircularReferenceResult
unwrapped := utils.UnwrapErrors(resolvingError)
circularReference = unwrapped[0].(*index.ResolvingError).CircularReference

// capture the journey with all details
var buf strings.Builder
for n := range circularReference.Journey {

	// add the full definition name to the journey.
	buf.WriteString(circularReference.Journey[n].Definition)
	if n < len(circularReference.Journey)-1 {
		buf.WriteString(" -> ")
	}
}

// print out the journey and the loop point.
fmt.Printf("Journey: %s\n", buf.String())
fmt.Printf("Loop Point: %s", circularReference.LoopPoint.Definition)
Output:

Journey: #/components/schemas/Two -> #/components/schemas/One -> #/components/schemas/Two
Loop Point: #/components/schemas/Two
// How to read in an OpenAPI 3 Specification, into a Document,
// modify the document and then re-render it back to YAML bytes.

// load an OpenAPI 3 specification from bytes
petstore, _ := os.ReadFile("test_specs/petstorev3.json")

// create a new document from specification bytes
doc, err := NewDocument(petstore)
// if anything went wrong, an error is thrown
if err != nil {
	panic(fmt.Sprintf("cannot create new document: %e", err))
}

// because we know this is a v3 spec, we can build a ready to go model from it.
v3Model, errors := doc.BuildV3Model()

// if anything went wrong when building the v3 model, a slice of errors will be returned
if errors != nil {
	fmt.Printf("error: %e\n", errors)
	panic(fmt.Sprintf("cannot create v3 model from document: %e", errors))
}

// create a new path item and operation.
newPath := &v3high.PathItem{
	Description: "this is a new path item",
	Get: &v3high.Operation{
		Description: "this is a get operation",
		OperationId: "getNewThing",
		RequestBody: &v3high.RequestBody{
			Description: "this is a new request body",
		},
	},
}

// capture original number of paths
originalPaths := orderedmap.Len(v3Model.Model.Paths.PathItems)

// add the path to the document
v3Model.Model.Paths.PathItems.Set("/new/path", newPath)

// render the document back to bytes and reload the model.
rawBytes, _, newModel, errs := doc.RenderAndReload()

// if anything went wrong when re-rendering the v3 model, a slice of errors will be returned
if errors != nil {
	panic(fmt.Sprintf("cannot re-render document: %e", errs))
}

// capture new number of paths after re-rendering
newPaths := orderedmap.Len(newModel.Model.Paths.PathItems)

// print the number of paths and schemas in the document
fmt.Printf("There were %d original paths. There are now %d paths in the document\n", originalPaths, newPaths)
fmt.Printf("The new spec has %d bytes\n", len(rawBytes))
Output:

There were 13 original paths. There are now 14 paths in the document
The new spec has 31406 bytes
// How to mutate values in an OpenAPI Specification, without re-ordering original content.

// create very small, and useless spec that does nothing useful, except showcase this feature.
spec := `
openapi: 3.1.0
info:
  title: This is a title
  contact:
    name: Some Person
    email: some@emailaddress.com
  license:
    url: https://some-place-on-the-internet.com/license
`
// create a new document from specification bytes
document, err := NewDocument([]byte(spec))
// if anything went wrong, an error is thrown
if err != nil {
	panic(fmt.Sprintf("cannot create new document: %e", err))
}

// because we know this is a v3 spec, we can build a ready to go model from it.
v3Model, err := document.BuildV3Model()

// if anything went wrong when building the v3 model, a slice of errors will be returned
if err != nil {
	fmt.Printf("error: %e\n", err)
	panic(fmt.Sprintf("cannot create v3 model from document: %e", err))
}

// mutate the title, to do this we currently need to drop down to the low-level API.
v3Model.Model.GoLow().Info.Value.Title.Mutate("A new title for a useless spec")

// mutate the email address in the contact object.
v3Model.Model.GoLow().Info.Value.Contact.Value.Email.Mutate("buckaroo@pb33f.io")

// mutate the name in the contact object.
v3Model.Model.GoLow().Info.Value.Contact.Value.Name.Mutate("Buckaroo")

// mutate the URL for the license object.
v3Model.Model.GoLow().Info.Value.License.Value.URL.Mutate("https://pb33f.io/license")

// serialize the document back into the original YAML or JSON
mutatedSpec, serialError := document.Serialize()

// if something went wrong serializing
if serialError != nil {
	panic(fmt.Sprintf("cannot serialize document: %e", serialError))
}

// print our modified spec!
fmt.Println(string(mutatedSpec))
Output:

openapi: 3.1.0
info:
    title: A new title for a useless spec
    contact:
        name: Buckaroo
        email: buckaroo@pb33f.io
    license:
        url: https://pb33f.io/license

If you're using complex types with OpenAPI Extensions, it's simple to unpack extensions into complex types using `high.UnpackExtensions()`. libopenapi retains the original raw data in the low model (not the high) which means unpacking them can be a little complex.

This example demonstrates how to use the `UnpackExtensions` with custom OpenAPI extensions.

// define an example struct representing a cake
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
}

// define a struct representing a burger
type burger struct {
	Sauce string
	Patty string
}

// define a struct that holds a map of cake pointers
type burgers struct {
	Description string
	Burgers     map[string]*burger
}

// create a specification with a schema and parameter that use complex custom cakes and burgers extensions.
spec := `openapi: "3.1"
components:
  schemas:
    SchemaOne:
      description: "Some schema with custom complex extensions"
      x-custom-cakes:
        description: some cakes
        cakes:
          someCake:
            candles: 10
            frosting: blue
            someStrangeVarName: something
          anotherCake:
            candles: 1
            frosting: green
  parameters:
    ParameterOne:
      description: "Some parameter also using complex extensions"
      x-custom-burgers:
        description: some burgers
        burgers:
          someBurger:
            sauce: ketchup
            patty: meat
          anotherBurger:
            sauce: mayo
            patty: lamb`
// create a new document from specification bytes
doc, err := NewDocument([]byte(spec))
// if anything went wrong, an error is thrown
if err != nil {
	panic(fmt.Sprintf("cannot create new document: %e", err))
}

// build a v3 model.
docModel, errs := doc.BuildV3Model()

// if anything went wrong building, indexing and resolving the model, an error is thrown
if errs != nil {
	panic(fmt.Sprintf("cannot create new document: %e", err))
}

// get a reference to SchemaOne and ParameterOne
schemaOne := docModel.Model.Components.Schemas.GetOrZero("SchemaOne").Schema()
parameterOne := docModel.Model.Components.Parameters.GetOrZero("ParameterOne")

// unpack schemaOne extensions into complex `cakes` type
schemaOneExtensions, schemaUnpackErrors := high.UnpackExtensions[cakes, *low.Schema](schemaOne)
if schemaUnpackErrors != nil {
	panic(fmt.Sprintf("cannot unpack schema extensions: %e", err))
}

// unpack parameterOne into complex `burgers` type
parameterOneExtensions, paramUnpackErrors := high.UnpackExtensions[burgers, *v3.Parameter](parameterOne)
if paramUnpackErrors != nil {
	panic(fmt.Sprintf("cannot unpack parameter extensions: %e", err))
}

// extract extension by name for schemaOne
customCakes := schemaOneExtensions.GetOrZero("x-custom-cakes")

// extract extension by name for schemaOne
customBurgers := parameterOneExtensions.GetOrZero("x-custom-burgers")

// print out schemaOne complex extension details.
fmt.Printf("schemaOne '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,
)

// print out parameterOne complex extension details.
fmt.Printf("parameterOne 'x-custom-burgers' (%s) has %d burgers, 'anotherBurger' has %s sauce and a %s patty\n",
	customBurgers.Description,
	len(customBurgers.Burgers),
	customBurgers.Burgers["anotherBurger"].Sauce,
	customBurgers.Burgers["anotherBurger"].Patty,
)
Output:

schemaOne 'x-custom-cakes' (some cakes) has 2 cakes, 'someCake' has 10 candles and blue frosting
parameterOne 'x-custom-burgers' (some burgers) has 2 burgers, 'anotherBurger' has mayo sauce and a lamb patty

NewDocumentWithConfiguration is the same as NewDocument, except it's a convenience function that calls NewDocument under the hood and then calls SetConfiguration() on the returned Document.

func NewDocumentWithTypeCheck(specByteArray []byte, bypassCheck bool) (Document, error)

DocumentModel represents either a Swagger document (version 2) or an OpenAPI document (version 3) that is built from a parent Document.

OverlayResult contains the result of applying an overlay to a target document.

ApplyOverlay applies the overlay to the target document and returns the modified document. This is the primary entry point for an overlay application when working with Document objects.

The returned OverlayDocument uses the same configuration as the input document.

func ApplyOverlayFromBytes(document Document, overlayBytes []byte) (*OverlayResult, error)

ApplyOverlayFromBytes applies an overlay (provided as bytes) to the target document. This is a convenience function when you have a Document but the overlay as raw bytes.

The returned OverlayDocument uses the same configuration as the input document.

func ApplyOverlayFromBytesToSpecBytes(docBytes, overlayBytes []byte) (*OverlayResult, error)

ApplyOverlayFromBytesToSpecBytes applies an overlay to target document bytes, where both the overlay and target document are provided as raw bytes. This is the most convenient function when you don't need to configure either document.

The returned OverlayDocument uses a default document configuration.

ApplyOverlayToSpecBytes applies the overlay to the target document bytes. Use this when you have raw spec bytes and a parsed Overlay object.

The returned OverlayDocument uses a default document configuration.