Applying OpenAPI Overlays
Transform OpenAPI specifications with the Overlay SpecificationThe 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
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)
}
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:
- Copy - Populate target with source content first
- Update - Override copied values with explicit updates
- 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.
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