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 values

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
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
itemEncoding 3.2

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