OpenAPI change detection

A diff engine and changelog detector for OpenAPI

OpenAPI 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 operationId of an operation
  • Deleting a Path
  • Changing a Schema type
  • Deleting an Operation from a Path
  • Adding/removing a Parameter from an Operation or Path

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:

There are 67 changes, of which 17 are breaking. 5 schemas have changes.

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 Path or Operation
  • Modifying operationId
  • Changing Schema type or format
  • Removing required Parameters
  • Modifying validation constraints (minimum, maximum, pattern, etc.)

Non-breaking by default:

  • Changes to description, summary, or title fields
  • Adding new Paths or Operations
  • Adding new optional properties to schemas
  • Changes to deprecated flag
  • Changes to example or examples values

This also includes plural examples collections on Parameter, Header, Response, MediaType, and Schema, plus added or removed entries under components.examples.

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 3.1 or 3.2 tag next to it.

Document-Level Rules

Property Added Modified Removed
openapi ✗ ✗ ✗
jsonSchemaDialect 3.1 ✗ ✗ ✗

Info

Property Added Modified Removed
title ✓ ✓ ✓
summary 3.1 ✓ ✓ ✓
description ✓ ✓ ✓
termsOfService ✓ ✓ ✓
version ✓ ✓ ✓
contact ✓ ✓ ✓
license ✓ ✓ ✓

Contact

Property Added Modified Removed
url ✓ ✓ ✓
name ✓ ✓ ✓
email ✓ ✓ ✓

License

Property Added Modified Removed
url ✓ ✓ ✓
name ✓ ✓ ✓
identifier 3.1 ✓ ✓ ✓

Paths

Property Added Modified Removed
path ✓ ✓ ✗

Path Item

Property Added Modified Removed
description ✓ ✓ ✓
summary ✓ ✓ ✓
get ✓ ✓ ✗
put ✓ ✓ ✗
post ✓ ✓ ✗
delete ✓ ✓ ✗
options ✓ ✓ ✗
head ✓ ✓ ✗
patch ✓ ✓ ✗
trace ✓ ✓ ✗
query 3.2 ✓ ✓ ✗
additionalOperations 3.2 ✓ ✓ ✗
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 ✓ ✓ ✓
examples ✓ ✓ ✓
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 3.2 ✗ ✓ ✗
example ✓ ✓ ✓
examples ✓ ✓ ✓
itemEncoding 3.2 ✓ ✓ ✗

Encoding

Property Added Modified Removed
contentType ✗ ✗ ✗
style ✗ ✗ ✗
explode ✗ ✗ ✗
allowReserved ✓ ✓ ✓

Header

Property Added Modified Removed
description ✓ ✓ ✓
style ✓ ✓ ✓
allowReserved ✓ ✓ ✓
allowEmptyValue ✗ ✗ ✗
explode ✓ ✓ ✓
example ✓ ✓ ✓
examples ✓ ✓ ✓
deprecated ✓ ✓ ✓
required ✗ ✗ ✗
schema ✗ ✓ ✗
items ✗ ✓ ✗

Schema

Property Added Modified Removed
$ref ✓ ✓ ✓
$dynamicAnchor 3.1 ✓ ✗ ✗
$dynamicRef 3.1 ✓ ✗ ✗
type ✓ ✗ ✓
title ✓ ✓ ✓
description ✓ ✓ ✓
format ✓ ✗ ✓
maximum ✓ ✗ ✓
minimum ✓ ✗ ✓
exclusiveMaximum ✓ ✗ ✓
exclusiveMinimum ✓ ✗ ✓
maxLength ✓ ✗ ✓
minLength ✓ ✗ ✓
pattern ✓ ✗ ✓
maxItems ✓ ✗ ✓
minItems ✓ ✗ ✓
maxProperties ✓ ✗ ✓
minProperties ✓ ✗ ✓
uniqueItems ✓ ✗ ✓
multipleOf ✓ ✗ ✓
contentEncoding 3.1 ✓ ✗ ✓
contentMediaType 3.1 ✓ ✗ ✓
default ✓ ✗ ✓
const 3.1 ✓ ✗ ✓
nullable ✓ ✗ ✓
readOnly ✓ ✗ ✓
writeOnly ✓ ✗ ✓
deprecated ✓ ✓ ✓
example ✓ ✓ ✓
examples 3.1 ✓ ✓ ✓
required ✗ ✓ ✗
enum ✓ ✓ ✗
properties ✓ ✓ ✗
additionalProperties ✗ ✗ ✗
allOf ✓ ✓ ✗
anyOf ✓ ✓ ✗
oneOf ✓ ✓ ✗
prefixItems 3.1 ✓ ✓ ✗
items ✗ ✗ ✗
discriminator ✗ ✓ ✗
externalDocs ✓ ✓ ✓
not ✗ ✓ ✗
if 3.1 ✗ ✓ ✗
then 3.1 ✗ ✓ ✗
else 3.1 ✗ ✓ ✗
propertyNames 3.1 ✗ ✓ ✗
contains 3.1 ✗ ✓ ✗
unevaluatedItems 3.1 ✗ ✓ ✗
unevaluatedProperties 3.1 ✗ ✗ ✗
dependentRequired 3.1 ✓ ✗ ✗
xml ✓ ✓ ✗
$schema 3.1 ✗ ✗ ✗

Discriminator

Property Added Modified Removed
propertyName ✗ ✗ ✗
mapping ✓ ✗ ✗
defaultMapping 3.2 ✗ ✗ ✗

XML

Property Added Modified Removed
nodeType 3.2 ✗ ✗ ✗
name ✗ ✗ ✗
namespace ✗ ✗ ✗
prefix ✗ ✗ ✗
attribute ✗ ✗ ✗
wrapped ✗ ✗ ✗

Server

Property Added Modified Removed
url ✗ ✗ ✗
description ✓ ✓ ✓
name 3.2 ✗ ✗ ✗

Server Variable

Property Added Modified Removed
enum ✓ ✓ ✗
default ✗ ✗ ✗
description ✓ ✓ ✓

Tag

Property Added Modified Removed
name ✓ ✗ ✗
summary 3.2 ✓ ✓ ✓
description ✓ ✓ ✓
parent 3.2 ✗ ✗ ✗
kind 3.2 ✓ ✓ ✓
externalDocs ✓ ✓ ✓

External Documentation

Property Added Modified Removed
url ✓ ✓ ✓
description ✓ ✓ ✓

Security Scheme

Property Added Modified Removed
type ✗ ✗ ✗
description ✓ ✓ ✓
name ✗ ✗ ✗
in ✗ ✗ ✗
scheme ✗ ✗ ✗
bearerFormat ✓ ✓ ✓
openIdConnectUrl ✓ ✓ ✓
oauth2MetadataUrl 3.2 ✓ ✓ ✓
flows ✓ ✓ ✗
deprecated 3.2 ✓ ✓ ✓
scopes ✓ ✓ ✗

Security Requirement

Property Added Modified Removed
schemes ✓ ✓ ✗
scopes ✓ ✓ ✗

OAuth Flows

Property Added Modified Removed
implicit ✓ ✓ ✗
password ✓ ✓ ✗
clientCredentials ✓ ✓ ✗
authorizationCode ✓ ✓ ✗
device 3.2 ✓ ✓ ✗

OAuth Flow

Property Added Modified Removed
authorizationUrl ✗ ✗ ✗
tokenUrl ✗ ✗ ✗
refreshUrl ✗ ✗ ✗
scopes ✓ ✗ ✗

Callback

Property Added Modified Removed
expressions ✓ ✓ ✗
Property Added Modified Removed
operationRef ✗ ✗ ✗
operationId ✗ ✗ ✗
requestBody ✗ ✗ ✗
description ✓ ✓ ✓
server ✗ ✓ ✗
parameters ✗ ✗ ✗

Example

Property Added Modified Removed
summary ✓ ✓ ✓
description ✓ ✓ ✓
value ✓ ✓ ✓
externalValue ✓ ✓ ✓
dataValue 3.2 ✓ ✓ ✓
serializedValue 3.2 ✓ ✓ ✓