Bundle OpenAPI references into a single file

Compact multiple OpenAPI references into a single file

Lots of OpenAPI specifications are exploded out across lots of different files. This is great for maintainability, but not so great for portability.

Lots of OpenAPI tools do not support exploded / multi-file OpenAPI specifications. Loading and locating references can be tricky, particularly when they are spread across local or remote file systems, so lots of tools just give up on trying to locate references and just ask for a single monolithic file.

libopenapi fully supports exploded / multi-file OpenAPI specifications.

Bundling raw bytes

The simplest way to bundle multiple OpenAPI references into a single file is to use the BundleBytes function from the bundler package:

func BundleBytes(bytes []byte, configuration *datamodel.DocumentConfiguration) ([]byte, error)

This method takes a slice of bytes, and a DocumentConfiguration pointer and returns a slice of bytes and an error if something went wrong.

The returned bytes is a single, fully bundled OpenAPI specification, all external references are resolved and inlined.

Local (references inside the root document) references are not bundled, They stay in place.

Bundling a document

The BundleDocument function from the bundler package is a convenience function that takes a v3.Document pointer and returns a slice of bytes and an error if something went wrong:

func BundleDocument(model *v3.Document) ([]byte, error)

Here is an example of checking out the mother of all exploded specifications the Digital Ocean OpenAPI specification and bundling it into a single file:

import (
  "bytes"
  "github.com/pb33f/libopenapi"
  "github.com/pb33f/libopenapi/bundler"
  "github.com/pb33f/libopenapi/datamodel"
  "github.com/stretchr/testify/assert"
  "log"
  "log/slog"
  "os"
  "os/exec"
  "path/filepath"
  "runtime"
  "strings"
)

func main() {

  // check out the mother of all exploded specifications
  tmp, _ := os.MkdirTemp("", "openapi")
  cmd := exec.Command("git", "clone", "https://github.com/digitalocean/openapi", tmp)
  defer os.RemoveAll(filepath.Join(tmp, "openapi"))

  err := cmd.Run()
  if err != nil {
      log.Fatalf("cmd.Run() failed with %s\n", err)
  }

  spec, _ := filepath.Abs(filepath.Join(tmp+"/specification", "DigitalOcean-public.v2.yaml"))
  specBytes, _ := os.ReadFile(spec)

  doc, err := libopenapi.NewDocumentWithConfiguration([]byte(specBytes), &datamodel.DocumentConfiguration{
      BasePath:                tmp + "/specification",
      ExtractRefsSequentially: true,
      Logger: slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
          Level: slog.LevelWarn,
      })),
  }) 
  if err != nil {
      panic(err) 
  }

  v3Doc, errs := doc.BuildV3Model()
  if len(errs) > 0 {
      panic(errs)
  }

  bytes, e := bundler.BundleDocument(&v3Doc.Model)

 //... do something with the bytes
}

And now we have a single, bundled OpenAPI specification that we can use with any tool that doesn’t support exploded specs.