Prevent unintentional breaking API changes in FastAPI apps
June 2, 2026
Things are changing all the time.
It's no different with APIs.
As we develop our products, APIs need to be updated as well.
Everything is great until we introduce an unintentional breaking change.
For example, if we rename the attribute in the response.
Let's say we rename cost to price.
All clients that use the cost attribute will break as soon as we deploy our changes.
They'll still try to read the cost attribute, but there won't be one with that name, so it will fail.
Nowadays, with all the AI tooling available, it's even more likely that PR introduces an unintentional breaking change.
In this post, we'll take a look at how to prevent that from happening.
You can find the example repository here
OpenAPI schema
OpenAPI is the most common standard for documenting APIs. You can use it to describe things like:
- all the endpoints that exist inside the API
- what are the query and path parameters of each endpoint
- how the responses look
- how the request payload should look
- what's used for authentication
FastAPI generates an OpenAPI schema for you out of your endpoint definitions together with type hints. That's great, as there's no need to write it manually. It's always in sync with the actual state. We can use that to detect breaking changes.
We can do the following:
- get OpenAPI json from the main branch
- get OpenAPI json from PR's branch
- Compare the schemas to see whether there are any breaking changes
Fortunately, there are great tools that we can use to detect breaking API changes. One of them is oasdiff.
Become a better engineer, one article at a time.
Practices, mindsets, and habits that actually move the needle. Delivered weekly to your inbox.
Adding oasdiff to CI/CD
oasdiff is "a command-line and Go package to compare and detect breaking changes in OpenAPI specs." Since we're in the Python and FastAPI world, we should use the CLI part. oasdiff provides many rules that you can customize. You can find the full list here. For now, we'll just use the default setup.
Adding oasdiff to CI/CD in GitHub Actions is simple:
name: CI
on:
pull_request:
branches: [main]
jobs:
breaking-changes:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v6
with:
ref: main
path: main-branch
- uses: astral-sh/setup-uv@v8.1.0
with:
python-version: "3.14"
- name: Generate schema from PR branch
run: |
uv sync
uv run python scripts/export_openapi.py new.json
- name: Generate schema from main branch
working-directory: main-branch
run: |
uv sync
uv run python scripts/export_openapi.py ../old.json
- name: Install oasdiff
run: |
curl -fsSL https://raw.githubusercontent.com/oasdiff/oasdiff/main/install.sh | sh
- name: Check for breaking changes
run: oasdiff breaking old.json new.json --fail-on ERR
# scripts/export_openapi.py
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from app.main import app
if __name__ == "__main__":
dest = sys.argv[1] if len(sys.argv) > 1 else "/dev/stdout"
with open(dest, "w") as f:
json.dump(app.openapi(), f, indent=2)
This workflow will run for every PR to the main branch.
First, it checks out the code from PR and the code from the main branch.
Then, it uses the export_openapi.py script to export the OpenAPI schema from the PR's branch.
After that, it uses the same script to get the OpenAPI schema from the main branch.
At the end, it uses oasdiff to compare the OpenAPI schemas and reports potential issues.
We need to add the --fail-on ERR flag to ensure a non-zero exit code in case of detected breaking changes.
Planned breaking changes
It's great to detect breaking changes, but currently, our job will fail if any are found. That means we won't be able to merge any PR that contains breaking API changes. That's not what we want. We just want to prevent accidental ones. Breaking API changes that are planned are fine if we ensure that all clients move to the new API version. For example, we should be allowed to remove older API versions once iOS and web clients have switched to the latest version. So, how to achieve that?
The simplest way is to skip the job when the [breaking-change] marker is present inside the PR title:
name: CI
on:
pull_request:
branches: [main]
jobs:
breaking-changes:
if: "!contains(github.event.pull_request.title, '[breaking-change]')" # NEW
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v6
with:
ref: main
path: main-branch
- uses: astral-sh/setup-uv@v8.1.0
with:
python-version: "3.14"
- name: Generate schema from PR branch
run: |
uv sync
uv run python scripts/export_openapi.py new.json
- name: Generate schema from main branch
working-directory: main-branch
run: |
uv sync
uv run python scripts/export_openapi.py ../old.json
- name: Install oasdiff
run: |
curl -fsSL https://raw.githubusercontent.com/oasdiff/oasdiff/main/install.sh | sh
- name: Check for breaking changes
run: oasdiff breaking old.json new.json --fail-on ERR
With that in place, we can now detect unexpected breaking changes and prevent such PRs from being merged. On the other hand, we can still proceed with planned breaking changes, such as the removal of legacy API endpoints.
Pro tip: Use strict types when typing the endpoints. This way, you'll detect breaking changes much more easily. For example, use enum if something is enum - don't use a string. With string, you might stop supporting
pendingstate sent in request body, but sincestateis defined as string, this won't be considered a breaking change.
Conclusion
Unintentional breaking changes can make our products fully or partially unusable. With a faster development pace due to AI tooling, it's even more likely to introduce such changes and miss them during code review. Fortunately, we can automate the check for breaking changes to ensure no unintentional ones find their way to production. At the same time, it's important to have a way to introduce them when they are planned and justified. Another piece you can add to your development workflow that will allow your team to move faster and more confidently.
Happy engineering!