in API Schemas
A breaking change in a REST API is not a versioning problem, it is a contract problem. When consumers rely on a schema and the provider silently changes it, production outages appear without warning. Technical enforcement means detecting breaking changes automatically, enforcing deprecation cycles and using consumer-driven contract tests as a structural safety net.
Table of Contents
- 1. What is a breaking change and what is not
- 2. Classifying breaking changes systematically
- 3. API versioning strategies compared
- 4. OpenAPI diff in the CI pipeline
- 5. Using deprecation headers and sunset correctly
- 6. Schema evolution without breaking changes
- 7. Consumer-driven contract tests with Pact
- 8. Introducing breaking changes under control
- 9. Monitoring API compatibility in production
- 10. Summary
- 11. FAQ
1. What is a breaking change and what is not
The line between a harmless update and a breaking change can be defined precisely in REST APIs, yet in practice it is often judged intuitively, and therefore unreliably. A breaking change is any change that forces existing consumers to adapt their code in order to keep using the API correctly. Renaming a field in the response body is a breaking change. Removing an optional field is a breaking change. Changing the data type of an existing field from string to integer is a breaking change.
What is not a breaking change: adding a new optional field to the response. Introducing a new optional query parameter. Adding a new HTTP status code for a new error case, as long as existing status codes remain unchanged. Introducing a new endpoint. Well-implemented API consumers write defensive code: they ignore unknown fields in the response and check only the fields they actually need. This pattern, known as the robustness principle or Postel's Law, is a prerequisite for stable schema evolution.
This systematic distinction matters because many teams introduce breaking changes accidentally, simply because they do not know exactly what counts as breaking. A clear, documented classification, ideally with automatic checking in the CI pipeline, is the foundation of any reliable backward compatibility strategy.
2. Classifying breaking changes systematically
Breaking changes in REST APIs fall into four categories: request-breaking (changes that make previously valid requests invalid), response-breaking (changes that alter the response structure), semantic-breaking (changes that alter the meaning of an endpoint without changing the schema) and protocol-breaking (changes to HTTP methods, status codes or auth mechanisms).
Semantic breaking changes are the most dangerous because they cannot be detected automatically. An endpoint that used to return all active users now, after a silent change, returns only users from the last 30 days, the schema is identical, the behavior has changed. This category requires consumer-driven contract tests that explicitly describe the expected behavior, not just the schema.
# Install openapi-diff for automated breaking change detection
npm install -g @openapitools/openapi-diff
# Compare two OpenAPI specs and detect breaking changes
openapi-diff old-openapi.json new-openapi.json
# Use oasdiff (Go-based, very fast)
brew install oasdiff
oasdiff breaking old-openapi.yaml new-openapi.yaml
# Integrate into CI: fail if breaking changes detected
oasdiff breaking old-openapi.yaml new-openapi.yaml --fail-on ERR
# Generate changelog between versions
oasdiff changelog old-openapi.yaml new-openapi.yaml --format text
3. API versioning strategies compared
API versioning is not an end in itself, it is the tool that makes breaking changes controllable. There are four common approaches: URI versioning (/api/v1/, /api/v2/), header versioning (Accept: application/vnd.api+json;version=2), query parameter versioning (?version=2) and content negotiation via media types. Each approach has different consequences for caching, routing, client implementation and maintenance effort.
URI versioning is the most popular approach because it is human-readable and is handled correctly by caching proxies. The downside: version numbers in URIs tempt teams to version too early and maintain the old version for too long. The best strategy is to avoid breaking changes for as long as possible through non-breaking schema evolution, and to introduce versioning only when fundamental redesigns are unavoidable. No versioning scheme replaces the discipline of minimizing breaking changes.
| Strategy | Caching | Visibility | Recommendation |
|---|---|---|---|
| URI /v1/ /v2/ | Optimal | Very high | Standard case, easy to handle |
| Accept header | Vary header required | Low | Clean, but more complex for clients |
| Query ?version= | Works | Medium | Simple, but semantically questionable |
| Media type vnd. | Complex | Very low | Only for hypermedia-mature APIs |
4. OpenAPI diff in the CI pipeline
The most effective mechanism for preventing unintentional breaking changes is automated diff checking in the CI pipeline. Tools such as oasdiff, openapi-diff or Bump.sh compare the OpenAPI specification of a pull request with the currently deployed production version and classify every change as non-breaking, breaking or potentially breaking. The build fails if breaking changes are introduced without explicit sign-off.
The check runs in two steps: first, the OpenAPI specification is generated from the source code (with API Platform via bin/console api:openapi:export, or by linting manually maintained specs with Spectral). Then the generated spec is compared against the most recently merged version on the main branch. Developers who need to introduce a breaking change can flag it explicitly with a release label, the build then accepts the change, but automatically generates a changelog entry and triggers the deprecation workflow.
5. Using deprecation headers and sunset correctly
HTTP defines standardized mechanisms for announcing API changes. The Deprecation header (RFC 8594) signals that an endpoint or feature is considered obsolete and will be removed in the future. The Sunset header (RFC 8594) states the exact date after which the endpoint will no longer be available. The Link header with rel="deprecation" points to the migration documentation. Together these three headers form a machine-readable deprecation communication channel.
Well-implemented API clients log deprecation headers automatically and alert developers when deprecated endpoints are called. In Symfony this behavior can be implemented via an event subscriber that writes a log entry on every response carrying a deprecation header. The combination of HTTP standard headers and automated monitoring ensures that the sunset date is never a surprise for API consumers.
# Example HTTP response headers for a deprecated endpoint
# Deprecation: Tue, 01 Oct 2026 00:00:00 GMT
# Sunset: Tue, 01 Jan 2027 00:00:00 GMT
# Link: <https://api.mironsoft.de/migration/v2>; rel="deprecation"
# Check deprecation headers on live API with curl
curl -I https://api.mironsoft.de/v1/products/1 \
-H "Accept: application/json" | grep -E "Deprecation|Sunset|Link"
# Monitor deprecated endpoint usage in application logs
grep "Deprecated endpoint called" /var/log/app/api.log | \
awk '{print $5}' | sort | uniq -c | sort -rn
# Set Sunset date in Symfony response (EventSubscriber approach)
# $response->headers->set('Deprecation', 'Tue, 01 Oct 2026 00:00:00 GMT');
# $response->headers->set('Sunset', 'Tue, 01 Jan 2027 00:00:00 GMT');
6. Schema evolution without breaking changes
The most effective strategy for enforcing backward compatibility is to avoid breaking changes from the outset. This is achieved through techniques of additive schema evolution: new fields are added as optional. Existing fields are never renamed, instead a new field with the desired name is added, while the old one remains in place as deprecated. Value enumerations are only ever extended, never shrunk.
A particularly effective pattern is the expand and contract approach for request parameters: first the new parameter is introduced as optional (expand). Once all consumers have migrated, the old parameter is marked as deprecated. Only after the sunset date is the old parameter removed (contract). This pattern requires coordination and monitoring, but it makes breaking changes avoidable, without giving up the desired schema improvement.
7. Consumer-driven contract tests with Pact
Pact is the best-known framework for consumer-driven contract tests. The idea: it is not the API provider who defines what is compatible, instead every consumer describes in a machine-readable "pact" document which requests it makes and which responses it expects. The provider service verifies at every release that it satisfies all pacts of all known consumers. A breaking change becomes visible immediately, before it hits consumers.
Pact works with a central broker, the Pact Broker (SaaS or self-hosted), which stores all pact documents and tracks the verification results. The can-i-deploy command of the Pact Broker answers the question "can provider v2.3 be deployed safely without breaking consumers?" with a machine-readable yes or no. This check is run as the last step before every deployment in the CI pipeline and prevents breaking changes from reaching production unnoticed.
8. Introducing breaking changes under control
When a breaking change is unavoidable, its controlled introduction follows a structured process. First, the new version of the endpoint is introduced alongside the old one (two versions available at the same time). Then the old version is marked with deprecation and sunset headers. Consumers are contacted actively and given a migration window. After the sunset date, the old version is disabled in the application, initially returning a 410 Gone response instead of a 404, which tells consumers still calling the old version that the endpoint was removed deliberately.
The most critical step is monitoring during the migration window: how many requests still hit the old version? From which clients? Which teams still need to migrate? Without this monitoring it is impossible to set the sunset date responsibly. API gateways such as Kong, AWS API Gateway or Nginx logs supply this data. A dashboard showing API version usage over time is the single most important tool for managing breaking-change migrations.
# Analyze which API versions are still in use (Nginx log analysis)
awk '{print $7}' /var/log/nginx/api-access.log | \
grep "^/api/" | \
sed 's|/api/\(v[0-9]*\)/.*|\1|' | \
sort | uniq -c | sort -rn
# Check if any clients still hit deprecated v1 endpoints
grep "/api/v1/" /var/log/nginx/api-access.log | \
awk '{print $1, $7}' | tail -100
# After sunset: return 410 Gone for removed endpoints
# In Symfony routing: configure a catch-all for /api/v1/*
# that returns JSON: {"error": "API v1 has been sunset. Please migrate to /api/v2/"}
# Run Pact can-i-deploy check before every deployment
./vendor/bin/pact-broker can-i-deploy \
--pacticipant "ProductAPI" \
--version "2.3.0" \
--to-environment production \
--broker-base-url https://pact-broker.mironsoft.de
9. Monitoring API compatibility in production
Backward compatibility does not end at deployment, it must be monitored continuously in production. That means monitoring error rates per API version, tracking still-used deprecated endpoints, and alerting when a consumer calls an endpoint that will reach its sunset date in less than 30 days. This information comes from access logs, API gateway metrics and application performance monitoring systems.
A practical tool is the API compatibility dashboard: for each API version it shows the number of active consumers, the call frequency of deprecated endpoints, and the remaining time until the next sunset date. This dashboard is not only relevant for the development team, it is the most important communication tool with consumer teams, partners and stakeholders who need to know when they must migrate.
10. Summary
Enforcing backward compatibility in REST API schemas technically means running four mechanisms at the same time: automatic breaking-change detection with OpenAPI diff tools in the CI pipeline. Deprecation communication via standardized HTTP headers (Deprecation, Sunset, Link). Consumer-driven contract tests with Pact, which ensure that no deployment breaks consumers. Migration monitoring, which shows which consumers still need to migrate and whether the sunset date is realistic.
The technical tools are only as effective as the discipline to apply them consistently. An OpenAPI diff check that can be bypassed manually protects nothing. A deprecation header without monitoring is pointless. Consumer-driven contract tests without active participation from the consumer teams are empty. Backward compatibility is not a technical problem alone, it is a coordination task that needs technical tools as its foundation and team discipline as its superstructure.
Mironsoft
API design, backward compatibility and migration strategy
Want to structurally enforce API compatibility?
We analyze your API interfaces, set up OpenAPI diff in the CI pipeline and implement consumer-driven contract tests with Pact, so that no breaking change reaches production unnoticed.
Breaking change audit
Analysis of existing APIs for undocumented breaking changes and versioning gaps
CI diff pipeline
Integrate oasdiff or openapi-diff into GitHub Actions/GitLab CI with automatic changelog
Pact integration
Set up consumer-driven contract tests and check can-i-deploy before every deployment
Backward Compatibility in API Schemas: the key points at a glance
Breaking vs. non-breaking
Renaming/removing fields = breaking. Adding optional fields = non-breaking. Extending enumerations = non-breaking. Changing types = breaking.
OpenAPI diff in CI
oasdiff or openapi-diff compare specs automatically. The build fails on unflagged breaking changes. A changelog is generated.
Deprecation headers
Deprecation + Sunset + Link headers (RFC 8594) are machine-readable. Clients log them automatically. The sunset date is communicated reliably.
Consumer-driven Pact
Consumers define their expectations as a pact. The provider verifies all pacts before every deployment. can-i-deploy as the final CI check.
11. FAQ: Backward Compatibility in API Schemas
1What is a breaking change?
2Is a new field in the response a breaking change?
3Which versioning strategy is recommended?
4oasdiff vs. openapi-diff?
5How long should a deprecated endpoint remain available?
6Deprecation vs. Sunset header?
7How does Pact work?
8What happens after the sunset date?
9Detecting semantic breaking changes automatically?
10What is expand and contract?
Backward compatibility in REST APIs requires discipline and clear processes. With OpenAPI as the single source of truth, automated breaking-change checks in CI and an expand-contract migration pattern, API schemas can stay stable in the long run.