OpenAPI change detection
A diff engine and changelog detector for OpenAPIOpenAPI change detection is a feature of libopenapi that allows you to detect changes between two
OpenAPI models. It’s the core engine that powers our openapi-changes
tool.
What is a change?
A change in libopenapi is defined as…
A property or object state was changed in one of the following ways:
- Modified
- Added
- Removed
A change has a binary breaking state as well. It can either be a non-breaking or a breaking change.
What is a breaking change?
A breaking change is any change to the OpenAPI contract that would cause a consuming client or user of that API to break.
A break means that client using the OpenAPI specification as a contract to generate language bindings, or build automation against, will fail to compile or will now run incorrectly because of the change.
It can also mean anyone expecting the same results after the change, will need to change their code.
Breaking examples
- Modifying the
operationIdof an operation - Deleting a
Path - Changing a
Schematype - Deleting an
Operationfrom aPath - Adding/removing a
Parameterfrom anOperationorPath
There are many more examples than this however.
Comparing documents
The libopenapi.CompareDocuments function is the main entry point for comparing two OpenAPI documents. It takes two
arguments, a left (original) and right (updated) document, and returns a libopenapi.DocumentChanges object.
import(
"fmt"
"io/ioutil"
"github.com/pb33f/libopenapi"
func main() {
// How to compare two different OpenAPI specifications.
// load an original OpenAPI 3 specification from bytes
burgerShopOriginal, _ := ioutil.ReadFile("test_specs/burgershop.openapi.yaml")
// load an **updated** OpenAPI 3 specification from bytes
burgerShopUpdated, _ := ioutil.ReadFile("test_specs/burgershop.openapi-modified.yaml")
// create a new document from original specification bytes
originalDoc, err := libopenapi.NewDocument(burgerShopOriginal)
// if anything went wrong, an error is thrown
if err != nil {
panic(fmt.Sprintf("cannot create new document: %e", err))
}
// create a new document from updated specification bytes
updatedDoc, err := libopenapi.NewDocument(burgerShopUpdated)
// if anything went wrong, an error is thrown
if err != nil {
panic(fmt.Sprintf("cannot create new document: %e", err))
}
// Compare documents for all changes made
documentChanges, err := libopenapi.CompareDocuments(originalDoc, updatedDoc)
// If anything went wrong when building models for documents.
if err != nil {
panic(fmt.Sprintf("cannot compare documents: %e", err))
}
// Extract SchemaChanges from components changes.
schemaChanges := documentChanges.ComponentsChanges.SchemaChanges
// Print out some interesting stats about the OpenAPI document changes.
fmt.Printf("There are %d changes, of which %d are breaking. %v schemas have changes.",
documentChanges.TotalChanges(), documentChanges.TotalBreakingChanges(), len(schemaChanges))
This would print out something like:
The DocumentChanges struct
All models for what-changed are in the what-changed/model package/directory.
The model.DocumentChanges
struct is the main object returned by the libopenapi.CompareDocuments function. It represents all the changes
made to the OpenAPI document and is structured in the same way as the OpenAPI document itself.
Every OpenAPI object type is broken down into its own struct, and each struct has a Changes property that is a slice of
Change objects.
The Change
object is a generic object that can represent any change made to an OpenAPI object.
The OpenAPI change structs are really just there to help structure the changes in a way that makes sense
programmatically. The real data is encapsulated in the Change object.
All OpenAPI change structs contain the following two methods
TotalChanges()TotalBreakingChanges()
Both methods calculate a sum of the total number of changes and breaking changes in the object AND all its children.
Change object
| Property | Type | Description |
|---|---|---|
Context |
ChangeContext |
Represents the lines and column numbers of the original and new values. |
ChangeType |
ChangeType |
Represents the type of change that occurred. stored as an integer. |
Property |
string |
The property name key being changed. |
Original |
string |
The original value represented as a string. |
New |
string |
The new value represented as a string. |
Breaking |
bool |
Determines if the change is a breaking one or not. |
OriginalObject |
any |
OriginalObject Represents the original object that was changed. |
NewObject |
any |
NewObject Represents the new object that has been modified. |
Configurable Breaking Change Rules
By default, libopenapi uses a set of sensible default rules to determine what constitutes a breaking change.
However, different teams and organizations may have different opinions about what should be considered breaking.
Starting with version 0.29, you can customize these rules to match your specific requirements.
How it works
The breaking change rules system allows you to override the default behavior for any OpenAPI object property. Each property can have three change types configured:
- Added - Is adding this property/object breaking?
- Modified - Is modifying this property/object breaking?
- Removed - Is removing this property/object breaking?
Setting custom rules
Use SetActiveBreakingRulesConfig() to apply custom rules before comparing documents:
import (
"github.com/pb33f/libopenapi"
"github.com/pb33f/libopenapi/what-changed/model"
)
func main() {
// Create a custom config - only specify what you want to change
customRules := &model.BreakingRulesConfig{
Operation: &model.OperationRules{
// Make operationId changes non-breaking
OperationID: &model.BreakingChangeRule{
Added: boolPtr(false),
Modified: boolPtr(false),
Removed: boolPtr(false),
},
},
Schema: &model.SchemaRules{
// Make description changes breaking (stricter than default)
Description: &model.BreakingChangeRule{
Modified: boolPtr(true),
},
},
}
// Apply the custom rules
model.SetActiveBreakingRulesConfig(customRules)
// Now compare documents - custom rules will be used
changes, _ := libopenapi.CompareDocuments(originalDoc, updatedDoc)
// Reset to defaults when done (optional)
model.ResetActiveBreakingRulesConfig()
}
func boolPtr(b bool) *bool {
return &b
}
Resetting to defaults
To revert to the default breaking change rules:
model.ResetActiveBreakingRulesConfig()
Getting the current config
To inspect the currently active configuration:
config := model.GetActiveBreakingRulesConfig()
Sparse overrides
You only need to specify the rules you want to change. Any rules not specified in your custom config will fall back to the defaults. This makes it easy to make small adjustments without having to redefine the entire rule set.
// This only changes the operationId rules - everything else uses defaults
customRules := &model.BreakingRulesConfig{
Operation: &model.OperationRules{
OperationID: &model.BreakingChangeRule{
Modified: boolPtr(false), // Only change "modified" behavior
},
},
}
Default breaking change rules
Here are some key defaults that libopenapi uses:
Breaking by default:
- Removing a
PathorOperation - Modifying
operationId - Changing
Schematype or format - Removing required
Parameters - Modifying validation constraints (
minimum,maximum,pattern, etc.)
Non-breaking by default:
- Changes to
description,summary, ortitlefields - Adding new
Paths orOperations - Adding new optional properties to schemas
- Changes to
deprecatedflag - Changes to
examplevalues
Thread safety
The breaking rules configuration is thread-safe. You can safely call SetActiveBreakingRulesConfig()
and ResetActiveBreakingRulesConfig() from multiple goroutines.
Default Breaking Rules Reference
The following tables document all default breaking change rules. A ✗ indicates the change type is considered breaking by default, while ✓ indicates it is non-breaking.
If a particular property is version specific, it has a
Document-Level Rules
| Property | Added | Modified | Removed |
|---|---|---|---|
openapi |
✗ | ✗ | ✗ |
jsonSchemaDialect |
✗ | ✗ | ✗ |
Info
| Property | Added | Modified | Removed |
|---|---|---|---|
title |
✓ | ✓ | ✓ |
summary |
✓ | ✓ | ✓ |
description |
✓ | ✓ | ✓ |
termsOfService |
✓ | ✓ | ✓ |
version |
✓ | ✓ | ✓ |
contact |
✓ | ✓ | ✓ |
license |
✓ | ✓ | ✓ |
Contact
| Property | Added | Modified | Removed |
|---|---|---|---|
url |
✓ | ✓ | ✓ |
name |
✓ | ✓ | ✓ |
email |
✓ | ✓ | ✓ |
License
| Property | Added | Modified | Removed |
|---|---|---|---|
url |
✓ | ✓ | ✓ |
name |
✓ | ✓ | ✓ |
identifier |
✓ | ✓ | ✓ |
Paths
| Property | Added | Modified | Removed |
|---|---|---|---|
path |
✓ | ✓ | ✗ |
Path Item
| Property | Added | Modified | Removed |
|---|---|---|---|
description |
✓ | ✓ | ✓ |
summary |
✓ | ✓ | ✓ |
get |
✓ | ✓ | ✗ |
put |
✓ | ✓ | ✗ |
post |
✓ | ✓ | ✗ |
delete |
✓ | ✓ | ✗ |
options |
✓ | ✓ | ✗ |
head |
✓ | ✓ | ✗ |
patch |
✓ | ✓ | ✗ |
trace |
✓ | ✓ | ✗ |
query |
✓ | ✓ | ✗ |
additionalOperations |
✓ | ✓ | ✗ |
servers |
✓ | ✓ | ✗ |
parameters |
✓ | ✓ | ✗ |
Operation
| Property | Added | Modified | Removed |
|---|---|---|---|
tags |
✓ | ✓ | ✗ |
summary |
✓ | ✓ | ✓ |
description |
✓ | ✓ | ✓ |
deprecated |
✓ | ✓ | ✓ |
operationId |
✗ | ✗ | ✗ |
externalDocs |
✓ | ✓ | ✓ |
responses |
✓ | ✓ | ✗ |
parameters |
✓ | ✓ | ✗ |
security |
✓ | ✓ | ✗ |
requestBody |
✗ | ✓ | ✗ |
callbacks |
✓ | ✓ | ✗ |
servers |
✓ | ✓ | ✗ |
Parameter
| Property | Added | Modified | Removed |
|---|---|---|---|
name |
✗ | ✗ | ✗ |
in |
✗ | ✗ | ✗ |
description |
✓ | ✓ | ✓ |
required |
✓ | ✗ | ✓ |
allowEmptyValue |
✗ | ✗ | ✗ |
style |
✓ | ✓ | ✓ |
allowReserved |
✗ | ✗ | ✗ |
explode |
✓ | ✓ | ✓ |
deprecated |
✓ | ✓ | ✓ |
example |
✓ | ✓ | ✓ |
schema |
✗ | ✓ | ✗ |
items |
✗ | ✓ | ✗ |
Request Body
| Property | Added | Modified | Removed |
|---|---|---|---|
description |
✓ | ✓ | ✓ |
required |
✗ | ✗ | ✗ |
Responses
| Property | Added | Modified | Removed |
|---|---|---|---|
default |
✓ | ✓ | ✗ |
codes |
✓ | ✓ | ✗ |
Response
| Property | Added | Modified | Removed |
|---|---|---|---|
description |
✓ | ✓ | ✓ |
summary |
✓ | ✓ | ✓ |
schema |
✗ | ✓ | ✗ |
examples |
✓ | ✓ | ✓ |
Media Type
| Property | Added | Modified | Removed |
|---|---|---|---|
schema |
✗ | ✓ | ✗ |
itemSchema |
✗ | ✓ | ✗ |
example |
✓ | ✓ | ✓ |
itemEncoding |
✓ | ✓ | ✗ |
Encoding
| Property | Added | Modified | Removed |
|---|---|---|---|
contentType |
✗ | ✗ | ✗ |
style |
✗ | ✗ | ✗ |
explode |
✗ | ✗ | ✗ |
allowReserved |
✓ | ✓ | ✓ |
Header
| Property | Added | Modified | Removed |
|---|---|---|---|
description |
✓ | ✓ | ✓ |
style |
✓ | ✓ | ✓ |
allowReserved |
✓ | ✓ | ✓ |
allowEmptyValue |
✗ | ✗ | ✗ |
explode |
✓ | ✓ | ✓ |
example |
✓ | ✓ | ✓ |
deprecated |
✓ | ✓ | ✓ |
required |
✗ | ✗ | ✗ |
schema |
✗ | ✓ | ✗ |
items |
✗ | ✓ | ✗ |
Schema
| Property | Added | Modified | Removed |
|---|---|---|---|
$ref |
✓ | ✓ | ✓ |
$dynamicAnchor |
✓ | ✗ | ✗ |
$dynamicRef |
✓ | ✗ | ✗ |
type |
✓ | ✗ | ✓ |
title |
✓ | ✓ | ✓ |
description |
✓ | ✓ | ✓ |
format |
✓ | ✗ | ✓ |
maximum |
✓ | ✗ | ✓ |
minimum |
✓ | ✗ | ✓ |
exclusiveMaximum |
✓ | ✗ | ✓ |
exclusiveMinimum |
✓ | ✗ | ✓ |
maxLength |
✓ | ✗ | ✓ |
minLength |
✓ | ✗ | ✓ |
pattern |
✓ | ✗ | ✓ |
maxItems |
✓ | ✗ | ✓ |
minItems |
✓ | ✗ | ✓ |
maxProperties |
✓ | ✗ | ✓ |
minProperties |
✓ | ✗ | ✓ |
uniqueItems |
✓ | ✗ | ✓ |
multipleOf |
✓ | ✗ | ✓ |
contentEncoding |
✓ | ✗ | ✓ |
contentMediaType |
✓ | ✗ | ✓ |
default |
✓ | ✗ | ✓ |
const |
✓ | ✗ | ✓ |
nullable |
✓ | ✗ | ✓ |
readOnly |
✓ | ✗ | ✓ |
writeOnly |
✓ | ✗ | ✓ |
deprecated |
✓ | ✓ | ✓ |
example |
✓ | ✓ | ✓ |
examples |
✓ | ✓ | ✓ |
required |
✗ | ✓ | ✗ |
enum |
✓ | ✓ | ✗ |
properties |
✓ | ✓ | ✗ |
additionalProperties |
✗ | ✗ | ✗ |
allOf |
✓ | ✓ | ✗ |
anyOf |
✓ | ✓ | ✗ |
oneOf |
✓ | ✓ | ✗ |
prefixItems |
✓ | ✓ | ✗ |
items |
✗ | ✗ | ✗ |
discriminator |
✗ | ✓ | ✗ |
externalDocs |
✓ | ✓ | ✓ |
not |
✗ | ✓ | ✗ |
if |
✗ | ✓ | ✗ |
then |
✗ | ✓ | ✗ |
else |
✗ | ✓ | ✗ |
propertyNames |
✗ | ✓ | ✗ |
contains |
✗ | ✓ | ✗ |
unevaluatedItems |
✗ | ✓ | ✗ |
unevaluatedProperties |
✗ | ✗ | ✗ |
dependentRequired |
✓ | ✗ | ✗ |
xml |
✓ | ✓ | ✗ |
$schema |
✗ | ✗ | ✗ |
Discriminator
| Property | Added | Modified | Removed |
|---|---|---|---|
propertyName |
✗ | ✗ | ✗ |
mapping |
✓ | ✗ | ✗ |
defaultMapping |
✗ | ✗ | ✗ |
XML
| Property | Added | Modified | Removed |
|---|---|---|---|
nodeType |
✗ | ✗ | ✗ |
name |
✗ | ✗ | ✗ |
namespace |
✗ | ✗ | ✗ |
prefix |
✗ | ✗ | ✗ |
attribute |
✗ | ✗ | ✗ |
wrapped |
✗ | ✗ | ✗ |
Server
| Property | Added | Modified | Removed |
|---|---|---|---|
url |
✗ | ✗ | ✗ |
description |
✓ | ✓ | ✓ |
name |
✗ | ✗ | ✗ |
Server Variable
| Property | Added | Modified | Removed |
|---|---|---|---|
enum |
✓ | ✓ | ✗ |
default |
✗ | ✗ | ✗ |
description |
✓ | ✓ | ✓ |
Tag
| Property | Added | Modified | Removed |
|---|---|---|---|
name |
✓ | ✗ | ✗ |
summary |
✓ | ✓ | ✓ |
description |
✓ | ✓ | ✓ |
parent |
✗ | ✗ | ✗ |
kind |
✓ | ✓ | ✓ |
externalDocs |
✓ | ✓ | ✓ |
External Documentation
| Property | Added | Modified | Removed |
|---|---|---|---|
url |
✓ | ✓ | ✓ |
description |
✓ | ✓ | ✓ |
Security Scheme
| Property | Added | Modified | Removed |
|---|---|---|---|
type |
✗ | ✗ | ✗ |
description |
✓ | ✓ | ✓ |
name |
✗ | ✗ | ✗ |
in |
✗ | ✗ | ✗ |
scheme |
✗ | ✗ | ✗ |
bearerFormat |
✓ | ✓ | ✓ |
openIdConnectUrl |
✓ | ✓ | ✓ |
oauth2MetadataUrl |
✓ | ✓ | ✓ |
flows |
✓ | ✓ | ✗ |
deprecated |
✓ | ✓ | ✓ |
scopes |
✓ | ✓ | ✗ |
Security Requirement
| Property | Added | Modified | Removed |
|---|---|---|---|
schemes |
✓ | ✓ | ✗ |
scopes |
✓ | ✓ | ✗ |
OAuth Flows
| Property | Added | Modified | Removed |
|---|---|---|---|
implicit |
✓ | ✓ | ✗ |
password |
✓ | ✓ | ✗ |
clientCredentials |
✓ | ✓ | ✗ |
authorizationCode |
✓ | ✓ | ✗ |
device |
✓ | ✓ | ✗ |
OAuth Flow
| Property | Added | Modified | Removed |
|---|---|---|---|
authorizationUrl |
✗ | ✗ | ✗ |
tokenUrl |
✗ | ✗ | ✗ |
refreshUrl |
✗ | ✗ | ✗ |
scopes |
✓ | ✗ | ✗ |
Callback
| Property | Added | Modified | Removed |
|---|---|---|---|
expressions |
✓ | ✓ | ✗ |
Link
| Property | Added | Modified | Removed |
|---|---|---|---|
operationRef |
✗ | ✗ | ✗ |
operationId |
✗ | ✗ | ✗ |
requestBody |
✗ | ✗ | ✗ |
description |
✓ | ✓ | ✓ |
server |
✗ | ✓ | ✗ |
parameters |
✗ | ✗ | ✗ |
Example
| Property | Added | Modified | Removed |
|---|---|---|---|
summary |
✓ | ✓ | ✓ |
description |
✓ | ✓ | ✓ |
value |
✓ | ✓ | ✓ |
externalValue |
✓ | ✓ | ✓ |
dataValue |
✓ | ✓ | ✓ |
serializedValue |
✓ | ✓ | ✓ |