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.1.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.1.0
info:
  title: My Overlay
  version: 1.0.0
  description: This overlay customizes the API for production deployment
actions:
  - target: $.info
    update:
      title: Updated API Title
  - target: $.paths./deprecated-endpoint
    remove: true

Action types

There are three types of actions:

  • Update - Merge new content into targeted nodes
  • Remove - Delete targeted nodes from the document
  • Copy - Populate target nodes with content from another location in the document

Parsing Overlays

Use NewOverlayDocument to parse an overlay from bytes:

import "github.com/pb33f/libopenapi"

func main() {
    overlayYAML := []byte(`overlay: 1.1.0
info:
  title: Production Overlay
  version: 1.0.0
  description: Customizes API for production environment
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("Description: %s\n", overlay.Info.Description)
    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.1.0
info:
  title: Update Title
  version: 1.0.0
  description: Updates API title and adds description
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

Copy Actions

Copy actions populate target nodes with content from another location in the document. The copy source is specified using a JSONPath expression that must select exactly one node.

Basic copy

Copy content from one location to another:

actions:
  # Copy the GET response schema to the POST response
  - target: $.paths['/users'].post.responses['201']
    copy: $.paths['/users'].get.responses['200']

Copy with update override

When an action has both copy and update, the copy happens first, then update overrides specific values:

actions:
  # Copy the entire operation, then override the summary
  - target: $.paths['/v2/users'].get
    copy: $.paths['/v1/users'].get
    update:
      summary: Version 2 - Get users
      description: Enhanced user retrieval with additional fields

This is useful for creating similar operations while customizing specific properties.

Copy and remove (move pattern)

Combine copy with remove in separate actions to move content:

actions:
  # First: copy old endpoint to new location
  - target: $.paths['/api/v2/users']
    copy: $.paths['/api/v1/users']

  # Second: remove the old endpoint
  - target: $.paths['/api/v1/users']
    remove: true

This pattern effectively moves content from one location to another.

Copy constraints

The copy operation has important constraints:

  • Single node: The copy source JSONPath must match exactly one node (not zero, not multiple)
  • Type compatibility: Source and target must be the same type (both objects, both arrays, or both primitives)

If these constraints are violated, an error is returned (see Error Handling below).

Operation order within an action

When a single action has multiple operations, they execute in this order:

  1. Copy - Populate target with source content first
  2. Update - Override copied values with explicit updates
  3. Remove - Clean up afterwards

This order enables the patterns shown above (copy+update override, copy+remove move).


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.1.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.

Within a single action, operations execute in a fixed order: copy → update → remove. This means you can copy content, override parts with update, and clean up with remove all in one action.

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.ErrPrimitiveTarget) {
    fmt.Println("Cannot update a primitive value directly")
}

Copy source not found

The copy source JSONPath matched zero nodes:

result, err := libopenapi.ApplyOverlay(doc, overlay)
if errors.Is(err, overlay.ErrCopySourceNotFound) {
    fmt.Println("Copy source path did not match any nodes")
}

Copy source multiple nodes

The copy source JSONPath must match exactly one node:

result, err := libopenapi.ApplyOverlay(doc, overlay)
if errors.Is(err, overlay.ErrCopySourceMultiple) {
    fmt.Println("Copy source matched multiple nodes; must match exactly one")
}

Copy type mismatch

The copy source and target must be the same type:

result, err := libopenapi.ApplyOverlay(doc, overlay)
if errors.Is(err, overlay.ErrCopyTypeMismatch) {
    fmt.Println("Copy source and target must be the same type (object/array/primitive)")
}

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.1.0
info:
  title: Production Overlay
  version: 1.0.0
  description: |
    Prepares the API specification for production deployment by:
    - Updating server URLs to production endpoints
    - Removing internal-only endpoints and extensions
    - Updating contact information
    - Standardizing error responses across endpoints    
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

  # Copy standard error response to all endpoints that don't have one
  - target: $.paths['/users'].get.responses['500']
    copy: $.components.responses.StandardError
  - target: $.paths['/orders'].get.responses['500']
    copy: $.components.responses.StandardError

  # 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