Applying OpenAPI Overlays

Transform OpenAPI specifications with the Overlay Specification

The OpenAPI Overlay Specification defines a standard way to modify OpenAPI documents without editing the original files. Overlays are useful for:

  • Environment-specific customization - Different server URLs for dev/staging/prod
  • API versioning - Removing deprecated operations for newer API versions
  • Partner customization - Tailoring documentation for specific integrations
  • Documentation updates - Adding descriptions without modifying source specs
  • SDK Generation - Add extensions to customize code generation
Overlay support was added in libopenapi v0.31. The implementation follows the Overlay Specification v1.0.0.

What is an Overlay?

An overlay is a YAML/JSON document that describes a sequence of actions to apply to a target OpenAPI specification. Each action uses JSONPath, and in particular RFC9535 to target specific nodes in the document.

Overlay structure

overlay: 1.0.0
info:
  title: My Overlay
  version: 1.0.0
actions:
  - target: $.info
    update:
      title: Updated API Title
  - target: $.paths./deprecated-endpoint
    remove: true

Action types

There are two types of actions:

  • Update - Merge new content into targeted nodes
  • Remove - Delete targeted nodes from the document

Parsing Overlays

Use NewOverlayDocument to parse an overlay from bytes:

import "github.com/pb33f/libopenapi"

func main() {
    overlayYAML := []byte(`overlay: 1.0.0
info:
  title: Production Overlay
  version: 1.0.0
actions:
  - target: $.servers
    update:
      - url: https://api.production.example.com`)

    overlay, err := libopenapi.NewOverlayDocument(overlayYAML)
    if err != nil {
        panic(err)
    }

    fmt.Printf("Overlay: %s v%s\n", overlay.Info.Title, overlay.Info.Version)
    fmt.Printf("Actions: %d\n", len(overlay.Actions))
}

Applying Overlays

There are four ways to apply overlays, depending on whether you’re working with Document objects or raw bytes.

ApplyOverlay

The primary entry point when working with parsed documents:

import (
    "fmt"
    "github.com/pb33f/libopenapi"
)

func main() {
    // Parse the target OpenAPI specification
    specYAML := []byte(`openapi: 3.1.0
info:
  title: Original API
  version: 1.0.0
paths:
  /users:
    get:
      summary: List users`)

    doc, err := libopenapi.NewDocument(specYAML)
    if err != nil {
        panic(err)
    }

    // Parse the overlay
    overlayYAML := []byte(`overlay: 1.0.0
info:
  title: Update Title
  version: 1.0.0
actions:
  - target: $.info
    update:
      title: My Updated API
      description: This API has been customized`)

    overlay, err := libopenapi.NewOverlayDocument(overlayYAML)
    if err != nil {
        panic(err)
    }

    // Apply the overlay using ApplyOverlay
    result, err := libopenapi.ApplyOverlay(doc, overlay)
    if err != nil {
        panic(err)
    }

    // The result contains the modified bytes
    fmt.Println(string(result.Bytes))

    // And a ready-to-use Document
    model, _ := result.OverlayDocument.BuildV3Model()
    fmt.Printf("New title: %s\n", model.Model.Info.Title)
}

This will output:

openapi: 3.1.0
info:
    title: My Updated API
    version: 1.0.0
    description: This API has been customized
paths:
    /users:
        get:
            summary: List users

ApplyOverlayFromBytes

When you have a Document but the overlay as raw bytes:

result, err := libopenapi.ApplyOverlayFromBytes(doc, overlayBytes)

ApplyOverlayToSpecBytes

When you have raw spec bytes and a parsed Overlay:

result, err := libopenapi.ApplyOverlayToSpecBytes(specBytes, overlay)

ApplyOverlayFromBytesToSpecBytes

The most convenient function when you don’t need to configure either document:

result, err := libopenapi.ApplyOverlayFromBytesToSpecBytes(specBytes, overlayBytes)

The OverlayResult

All apply functions return an OverlayResult:

type OverlayResult struct {
    // Bytes contains the raw YAML bytes of the modified document.
    Bytes []byte

    // OverlayDocument is the modified document, ready to have a model built from it.
    OverlayDocument Document

    // Warnings contains any warnings from overlay application (e.g., zero-match targets).
    Warnings []*overlay.Warning
}

Using OverlayResult

result, err := libopenapi.ApplyOverlay(doc, overlay)
if err != nil {
    panic(err)
}

// Check for warnings (non-fatal issues)
for _, warning := range result.Warnings {
    fmt.Printf("Warning: %s (target: %s)\n", warning.Message, warning.Target)
}

// Use the modified bytes directly
os.WriteFile("modified-spec.yaml", result.Bytes, 0644)

// Or build a model from the result document
model, errs := result.OverlayDocument.BuildV3Model()
if len(errs) > 0 {
    panic(errs)
}

// Access the modified specification
fmt.Printf("Title: %s\n", model.Model.Info.Title)

Configuration Preservation

When using ApplyOverlay or ApplyOverlayFromBytes, the resulting document inherits the configuration from the input document:

import (
    "github.com/pb33f/libopenapi"
    "github.com/pb33f/libopenapi/datamodel"
)

func main() {
    // Create document with custom configuration
    config := &datamodel.DocumentConfiguration{
        AllowFileReferences:   true,
        AllowRemoteReferences: false,
    }

    doc, _ := libopenapi.NewDocumentWithConfiguration(specBytes, config)

    // Apply overlay - configuration is preserved
    result, _ := libopenapi.ApplyOverlay(doc, overlay)

    // result.OverlayDocument has the same configuration
    resultConfig := result.OverlayDocument.GetConfiguration()
    fmt.Printf("AllowFileReferences: %v\n", resultConfig.AllowFileReferences)
}
When using ApplyOverlayToSpecBytes or ApplyOverlayFromBytesToSpecBytes, the resulting document uses default configuration since there is no input document to inherit from.

Update Actions

Update actions merge content into targeted nodes. The merge behavior depends on the node type.

Object merge

When targeting an object, properties are recursively merged:

# Target document
info:
  title: Original
  version: 1.0.0

# Overlay action
actions:
  - target: $.info
    update:
      title: Updated
      description: Added description

# Result
info:
  title: Updated
  version: 1.0.0
  description: Added description

Array append

When targeting an array, update content is appended:

# Target document
tags:
  - name: users

# Overlay action
actions:
  - target: $.tags
    update:
      - name: admin

# Result
tags:
  - name: users
  - name: admin

Nested updates

Updates work on deeply nested paths:

actions:
  - target: $.paths./users.get.responses.200.description
    update: Successfully retrieved users

Remove Actions

Remove actions delete targeted nodes from the document:

actions:
  - target: $.paths./deprecated-endpoint
    remove: true
  - target: $.info.x-internal-notes
    remove: true
If an action has both update and remove: true, the remove takes precedence and the update is ignored.

JSONPath Targeting

Overlays use JSONPath expressions to target nodes. Common patterns include:

Pattern Description
$.info The info object
$.info.title The title property
$.paths./users A specific path
$.paths./users.get A specific operation
$.paths.* All paths
$.paths.*.get All GET operations
$.components.schemas.User A specific schema
$..description All description fields (recursive)

libopenapi supports all RFC9535 and JSON Path Plus annotations and query expressions.

Zero-match warnings

If a target matches zero nodes, a warning is generated, but processing continues:

result, err := libopenapi.ApplyOverlay(doc, overlay)
if err != nil {
    panic(err)
}

for _, warning := range result.Warnings {
   fmt.Printf("No match for: %s\n", warning.Target)
}

Sequential Actions

Actions are applied sequentially in order. Each action operates on the result of the previous action:

overlay: 1.0.0
info:
  title: Sequential Example
  version: 1.0.0
actions:
  # First: add a description
  - target: $.info
    update:
      description: Temporary description

  # Second: remove it (this works because action 1 already added it)
  - target: $.info.description
    remove: true

This allows for complex transformations where later actions depend on earlier modifications.


Error Handling

Overlay application can fail for several reasons:

Invalid overlay structure

overlay, err := libopenapi.NewOverlayDocument(invalidBytes)
if errors.Is(err, overlay.ErrInvalidOverlay) {
    fmt.Println("Invalid overlay document")
}

Invalid JSONPath

result, err := libopenapi.ApplyOverlay(doc, overlay)
if errors.Is(err, overlay.ErrInvalidJSONPath) {
    fmt.Println("Invalid JSONPath expression in action")
}

Invalid target type

Update actions require the target to be an object or array, not a primitive:

result, err := libopenapi.ApplyOverlay(doc, overlay)
if errors.Is(err, overlay.ErrInvalidTargetType) {
    fmt.Println("Cannot update a primitive value directly")
}

Complete Example

Here’s a complete example showing a production deployment workflow:

import (
    "fmt"
    "os"
    "github.com/pb33f/libopenapi"
)

func main() {
    // Read the base OpenAPI specification
    specBytes, _ := os.ReadFile("api-spec.yaml")

    // Read the production overlay
    overlayBytes, _ := os.ReadFile("overlays/production.yaml")

    // Parse the spec with configuration
    doc, err := libopenapi.NewDocument(specBytes)
    if err != nil {
        panic(fmt.Sprintf("Failed to parse spec: %v", err))
    }

    // Parse and apply the overlay
    overlay, err := libopenapi.NewOverlayDocument(overlayBytes)
    if err != nil {
        panic(fmt.Sprintf("Failed to parse overlay: %v", err))
    }

    result, err := libopenapi.ApplyOverlay(doc, overlay)
    if err != nil {
        panic(fmt.Sprintf("Failed to apply overlay: %v", err))
    }

    // Report any warnings
    if len(result.Warnings) > 0 {
        fmt.Printf("Applied with %d warnings:\n", len(result.Warnings))
        for _, w := range result.Warnings {
            fmt.Printf("  - %s: %s\n", w.Target, w.Message)
        }
    }

    // Write the modified specification
    os.WriteFile("dist/api-spec-production.yaml", result.Bytes, 0644)

    // Optionally validate the result
    model, errs := result.OverlayDocument.BuildV3Model()
    if len(errs) > 0 {
        fmt.Printf("Validation errors: %v\n", errs)
    } else {
        fmt.Printf("Successfully generated: %s v%s\n",
            model.Model.Info.Title,
            model.Model.Info.Version)
    }
}

Example overlay file

overlay: 1.0.0
info:
  title: Production Overlay
  version: 1.0.0
actions:
  # Update servers for production
  - target: $.servers
    update:
      - url: https://api.example.com
        description: Production server

  # Remove internal endpoints
  - target: $.paths./internal/health
    remove: true
  - target: $.paths./internal/metrics
    remove: true

  # Remove internal extensions
  - target: $..x-internal
    remove: true

  # Update contact info
  - target: $.info.contact
    update:
      name: API Support
      email: api-support@example.com
      url: https://support.example.com

Best Practices

  • Keep overlays focused - Create separate overlays for different purposes (environment, audience, version)
  • Test overlays in isolation - Verify each overlay produces the expected output before combining
  • Handle warnings - Zero-match warnings may indicate stale overlays that need updating
  • Version your overlays - Keep overlays in version control alongside your specifications