Swagger (now OpenAPI) definitions, when they break, don’t actually break the API; they break the clients and tools that rely on that definition to understand how to interact with the API.
Let’s see what happens when a simple API definition has a breaking change and how we can catch it.
Imagine we have a basic OpenAPI 3.0 definition for a simple pet store API:
openapi: 3.0.0
info:
title: Pet Store API
version: 1.0.0
servers:
- url: http://localhost:8080
paths:
/pets:
get:
summary: List all pets
operationId: listPets
responses:
'200':
description: A list of pets.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
components:
schemas:
Pet:
type: object
properties:
id:
type: integer
format: int64
name:
type: string
tag:
type: string
nullable: true
This definition is pretty straightforward. It describes a GET endpoint /pets that returns an array of Pet objects, where each Pet has an id, name, and an optional tag.
Now, let’s say we want to add a new field to the Pet schema, maybe species.
openapi: 3.0.0
info:
title: Pet Store API
version: 1.0.0
servers:
- url: http://localhost:8080
paths:
/pets:
get:
summary: List all pets
operationId: listPets
responses:
'200':
description: A list of pets.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
components:
schemas:
Pet:
type: object
properties:
id:
type: integer
format: int64
name:
type: string
tag:
type: string
nullable: true
species: # New field added
type: string
At first glance, this seems harmless. We’ve just added a new field. Most existing clients that consume this API and only care about id and name will happily continue to work because the species field is new and they can ignore it. However, this is a breaking change if we consider the OpenAPI definition as a contract for both consumers and producers, and if we’re generating code or documentation from it.
Consider the opposite: What if we remove a field, say tag?
openapi: 3.0.0
info:
title: Pet Store API
version: 1.0.0
servers:
- url: http://localhost:8080
paths:
/pets:
get:
summary: List all pets
operationId: listPets
responses:
'200':
description: A list of pets.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
components:
schemas:
Pet:
type: object
properties:
id:
type: integer
format: int64
name:
type: string
# tag: # Tag field removed
# type: string
# nullable: true
This is a definite breaking change. If a client was expecting tag and it’s no longer there, it will likely fail. More subtly, if you’re using code generation tools (like Swagger Codegen or OpenAPI Generator) to create client SDKs or server stubs, removing a field means the generated code will no longer have a corresponding property, and any client code that tries to access pet.tag will fail at compile time or runtime.
The core problem is that the OpenAPI definition is the source of truth. When it changes in a way that invalidates assumptions made by consumers or tools, the system breaks.
To detect these changes systematically, we use specialized tools that can compare two versions of an OpenAPI definition and highlight differences. The most popular and robust tool for this is openapi-diff.
Here’s how you’d typically use it in a CI pipeline:
-
Install
openapi-diff:npm install -g openapi-diff -
Create a CI job: In your CI/CD pipeline (e.g., GitHub Actions, GitLab CI, Jenkins), you’ll add a step to compare the OpenAPI definition from your main branch (or a previous release) with the one from the current branch.
Let’s assume your OpenAPI definition file is named
openapi.yaml.# Example GitHub Actions workflow snippet steps: - name: Checkout code uses: actions/checkout@v3 with: fetch-depth: 0 # Needed to compare across branches - name: Install openapi-diff run: npm install -g openapi-diff - name: Compare OpenAPI definitions run: | openapi-diff \ --old api/openapi.yaml \ --new api/openapi.yaml \ --output report.json \ --level 'error' # Or 'warning' or 'info'The
--oldand--newflags point to the same file here, but in a real CI scenario, you’d typically compare the file from themainbranch (ordevelop) against the file from the feature branch. For example:openapi-diff \ --old https://raw.githubusercontent.com/your-org/your-repo/main/api/openapi.yaml \ --new api/openapi.yaml \ --output report.json \ --level 'error' -
Configure
openapi-difflevel: The--levelflag is crucial.error: Only reports breaking changes that are likely to cause issues for clients.warning: Reports breaking changes and potentially problematic non-breaking changes.info: Reports all changes, including additive ones.
For CI checks, you’ll almost always want
level='error'orlevel='warning'. -
Fail the build: If
openapi-difffinds any issues at the specified level, it will exit with a non-zero status code, failing your CI job. You can then examine thereport.jsonfor details.
Common Breaking Changes and How openapi-diff Catches Them:
-
Removing a required field from a schema:
- Diagnosis:
openapi-diffwill reportschema.properties.<field_name>has been removed. - Fix: Re-add the field or mark it as
nullable: trueif it’s truly optional and clients might have been relying on its presence. - Why it works: Clients expecting this field will break if it’s gone. Making it nullable signals that its absence is permissible.
- Diagnosis:
-
Changing a field’s type (e.g.,
stringtointeger):- Diagnosis:
openapi-diffwill report a type mismatch forschema.properties.<field_name>. - Fix: Ensure the new type is compatible (e.g., if changing from
integertonumber, it’s usually fine; if changing fromstringtointeger, it’s a break). If it must change, coordinate with clients or use a more flexible type likeoneOfif possible. - Why it works: A client expecting a string and receiving an integer (or vice-versa) will likely cause a parsing error.
- Diagnosis:
-
Changing a parameter from optional to required:
- Diagnosis:
openapi-diffflagspaths.<path>.<method>.parameters.<parameter_name>as changing from optional to required. - Fix: Make the parameter optional again (remove
required: truefrom its definition) or ensure all clients are updated to provide this parameter. - Why it works: Clients that were omitting this optional parameter will now fail validation on the server or during client-side generation.
- Diagnosis:
-
Changing the type of a parameter:
- Diagnosis:
openapi-diffwill report a type mismatch forpaths.<path>.<method>.parameters.<parameter_name>.schema. - Fix: Ensure the new type is compatible or coordinate a rollout.
- Why it works: Similar to schema fields, clients sending data of the wrong type will cause validation errors.
- Diagnosis:
-
Removing an entire endpoint or operation:
- Diagnosis:
openapi-diffwill report that an entire path or HTTP method has been removed. - Fix: If the endpoint is truly gone, ensure clients are updated to not call it. If it’s a mistake, restore the path.
- Why it works: Clients attempting to call a non-existent endpoint will receive 404 Not Found errors.
- Diagnosis:
-
Changing response status codes: Removing a successful response code (e.g., removing
201 Createdwhen the API now only returns200 OKfor that operation) is a breaking change.- Diagnosis:
openapi-diffwill reportpaths.<path>.<method>.responses.<status_code>has been removed. - Fix: If the status code is essential for clients to distinguish outcomes, restore it or add a new response code. If it’s just a refactor, ensure clients are aware.
- Why it works: Clients might be specifically waiting for a
201to trigger a subsequent action.
- Diagnosis:
The most surprising aspect of OpenAPI breaking changes is how subtle they can be, especially with code generation. A change that seems minor to a human reader can cause generated client code to fail compilation or runtime if the code generator strictly adheres to the schema and doesn’t have robust backward compatibility strategies built-in. This is why openapi-diff is indispensable – it acts as an automated, rigorous reviewer of your API contract.
The next challenge after managing breaking changes is ensuring your API documentation stays in sync and is generated from the same OpenAPI file used for validation.