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.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)
}
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
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