Detect breaking changes between API schemas (OpenAPI / JSON Schema) in a deterministic, CI-friendly way.
api-schema-diff is a CLI tool designed to answer one question reliably:
"Will this schema change break existing clients?"
API changes often break clients silently.
Common causes:
- Removing endpoints
- Removing fields
- Changing parameter requirements
- Changing request/response schemas
api-schema-diff catches these issues before they reach production.
- OpenAPI 3.x (JSON / YAML)
- JSON Schema (generic structure diff)
- Removed paths or operations
- Removed query / path / header parameters
- Parameters becoming required
- Parameter schema type changes
- Request body removed
- Request body becoming required
- Request body schema breaking changes
- Removed response status codes
- Response schema breaking changes
- Removed properties in schemas
- Property type changes
- Optional → required fields
- Added paths or operations
- Added optional parameters
- Added optional request bodies
- Added response status codes
- Added optional properties
- Deterministic output
- Stable exit codes
- JSON output for automation
- Works offline (no cloud / no LLM dependency)
pip install api-schema-diffOr install from source:
git clone https://github.com/teolzr/schema-diff.git
cd schema-diff
pip install -e .Requirements:
- Python 3.10 or higher
typer>=0.12rich>=13.7
Run without installing Python:
# Pull the image
docker pull ghcr.io/teolzr/schema-diff:latest
# Run with local files
docker run --rm -v $(pwd):/workspace ghcr.io/teolzr/schema-diff:latest old.yaml new.yaml
# Check version
docker run --rm ghcr.io/teolzr/schema-diff:latest --version
# Use in CI/CD
docker run --rm \
-v $(pwd):/workspace \
ghcr.io/teolzr/schema-diff:latest \
schemas/v1.yaml schemas/v2.yaml \
--format jsonAvailable tags:
latest- Latest stable releasev0.1.4- Specific versionmain- Latest from main branch
Use in any GitHub workflow:
- uses: teolzr/schema-diff@v1
with:
old: api/schema-v1.yaml
new: api/schema-v2.yamlCompare against main branch:
- run: git show origin/main:api/schema.yaml > /tmp/baseline.yaml
- uses: teolzr/schema-diff@v1
with:
old: /tmp/baseline.yaml
new: api/schema.yamlJSON output:
- uses: teolzr/schema-diff@v1
with:
old: old.yaml
new: new.yaml
format: json
fail-on-breaking: falseapi-schema-diff old.json new.jsonExit codes:
0→ No breaking changes found1→ Breaking changes detected
Text output (default):
api-schema-diff old.json new.jsonOutput:
BREAKING CHANGES FOUND
┏━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Type ┃ Path ┃ Old Type┃ New Type┃ Message ┃
┡━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩
│ removed_field│ User.email │ │ │ │
│ type_change │ Order.amount│ <number>│ <string>│ │
└──────────────┴─────────────┴─────────┴─────────┴───────────────────────┘
JSON output:
api-schema-diff old.json new.json --format jsonOutput:
{
"breaking": [
{
"type": "removed_field",
"severity": "breaking",
"path": "User.email",
"old_type": null,
"new_type": null,
"message": null
},
{
"type": "type_change",
"severity": "breaking",
"path": "Order.amount",
"old_type": "<number>",
"new_type": "<string>",
"message": null
}
],
"non_breaking": []
}api-schema-diff [OPTIONS] OLD_FILE NEW_FILEArguments:
OLD_FILE- Path to the old schema file (JSON or YAML)NEW_FILE- Path to the new schema file (JSON or YAML)
Options:
--format [text|json]- Output format (default:text)--fail-on-breaking / --no-fail-on-breaking- Exit with code 1 when breaking changes are found (default:true)--help- Show help message
Use --no-fail-on-breaking to always exit with code 0 (useful for reporting without failing CI):
api-schema-diff old.json new.json --no-fail-on-breakingold.json:
{
"User": {
"email": "a@b.com",
"age": 30
},
"Order": {
"amount": 12.5
}
}new.json:
{
"User": {
"age": "30"
},
"Order": {
"amount": "12.5"
},
"NewThing": {}
}api-schema-diff old.json new.jsonDetected changes:
- Breaking:
User.emailfield removed - Breaking:
User.agetype changed from number to string - Breaking:
Order.amounttype changed from number to string - Non-breaking:
NewThingobject added
old-api.yaml:
openapi: 3.0.0
paths:
/users:
get:
parameters:
- name: limit
in: query
required: false
schema:
type: integer
responses:
'200':
description: Success
content:
application/json:
schema:
type: object
properties:
name:
type: string
email:
type: stringnew-api.yaml:
openapi: 3.0.0
paths:
/users:
get:
parameters:
- name: limit
in: query
required: true # Now required!
schema:
type: integer
responses:
'200':
description: Success
content:
application/json:
schema:
type: object
properties:
name:
type: string
# email removed!api-schema-diff old-api.yaml new-api.yamlDetected changes:
- Breaking: Query parameter
limitbecame required - Breaking: Response property
emailremoved
name: Schema Diff Check
on:
pull_request:
paths:
- 'api/schema.yaml'
jobs:
schema-diff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install api-schema-diff
run: pip install api-schema-diff
- name: Get old schema from main branch
run: git show origin/main:api/schema.yaml > old-schema.yaml
- name: Check for breaking changes
run: api-schema-diff old-schema.yaml api/schema.yamlapi-schema-diff:
image: python:3.10
before_script:
- pip install api-schema-diff
script:
- git show origin/main:api/schema.yaml > old-schema.yaml
- api-schema-diff old-schema.yaml api/schema.yaml
only:
changes:
- api/schema.yamlCreate .pre-commit-config.yaml:
repos:
- repo: local
hooks:
- id: api-schema-diff
name: Check API schema for breaking changes
entry: bash -c 'git show HEAD:api/schema.yaml > /tmp/old-schema.yaml && api-schema-diff /tmp/old-schema.yaml api/schema.yaml'
language: system
files: 'api/schema.yaml'
pass_filenames: false# Clone the repository
git clone https://github.com/teolzr/schema-diff.git
cd schema-diff
# Install in development mode
pip install -e .
# Install development dependencies
pip install pytest pytest-cov black ruff# Run all tests
pytest
# Run with coverage
pytest --cov=schema_diff --cov-report=html
# Run specific test file
pytest tests/test_openapi_diff.pyNote: The Python package is still named schema_diff internally, but the PyPI package and CLI command are api-schema-diff.
# Format code
black .
# Lint code
ruff check .schema-diff/
├── schema_diff/
│ ├── __init__.py
│ ├── cli.py # CLI entry point
│ ├── diff.py # Generic JSON diff logic
│ ├── loader.py # Schema file loading
│ ├── models.py # Data models
│ ├── rules.py # Breaking change rules
│ └── openapi/
│ ├── __init__.py
│ ├── diff.py # OpenAPI-specific diff
│ ├── json_schema_diff.py # JSON Schema diffing
│ ├── normalizer.py # Schema normalization
│ └── resolver.py # $ref resolution
├── tests/
├── pyproject.toml
└── README.md
- Schema loading: Automatically detects schema type (OpenAPI vs generic JSON/YAML)
- Normalization: Resolves
$refreferences and normalizes structure - Diffing: Compares schemas using rule-based detection
- Classification: Categorizes changes as breaking or non-breaking
- Reporting: Outputs results in human-readable or JSON format
Breaking changes:
- Removing existing fields/paths → clients expect them
- Changing types → clients may send wrong data type
- Making optional fields required → clients may not send them
- Removing response fields → clients may depend on them
Non-breaking changes:
- Adding new fields/paths → clients can ignore them
- Making required fields optional → clients can still send them
- Adding new optional fields → backwards compatible
- Support for OpenAPI 2.0 (Swagger)
- GraphQL schema diffing
- Custom rule configuration
- HTML report generation
- API compatibility scoring
- Severity levels (error, warning, info)
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes
- Add tests for new functionality
- Run tests and linting
- Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
MIT License - see LICENSE file for details
Built with:
- openapi-diff - OpenAPI comparison tool
- swagger-diff - Swagger API comparison
- json-schema-diff - JSON Schema comparison
- 🐛 Issues: GitHub Issues
- 💬 Discussions: GitHub Discussions
Note: Repository name is schema-diff, but PyPI package name is api-schema-diff