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, errs := libopenapi.CompareDocuments(originalDoc, updatedDoc)
    
    // If anything went wrong when building models for documents.
    if len(errs) > 0 {
        for i := range errs {
            fmt.Printf("error: %e\n", errs[i])
        }
        panic(fmt.Sprintf("cannot compare documents: %d errors reported", len(errs)))
    }
    
    // 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.