JSON API Versioning — Managing Breaking Changes in JSON APIs
JSON APIs need versioning when structure changes break existing clients. Here's how to version JSON APIs using URL versioning, headers, and response envelope strategies, with...
When a JSON API changes its response structure in a way that breaks existing clients, you need a versioning strategy. The goal: let old clients continue working while new clients can use updated response formats.
Use the JSON Diff Tool to compare API responses between versions.
What constitutes a breaking change
Breaking changes:
- Removing a field that clients depend on
- Renaming a field (
firstName→first_name) - Changing a field’s type (
"age": 30→"age": "thirty") - Changing array to object or vice versa
- Adding required fields to request bodies
Non-breaking changes:
- Adding new optional fields to responses
- Adding new optional request parameters
- Adding new endpoints
- Adding new values to an enum (clients should handle unknown values)
Versioning strategies
URL versioning (most common)
/v1/users/123
/v2/users/123
GET /v1/users/123
{ "firstName": "Alice", "lastName": "Smith" }
GET /v2/users/123
{ "first_name": "Alice", "last_name": "Smith", "fullName": "Alice Smith" }
Simple to implement, clear in URLs, easy to test different versions. Downside: duplicates endpoint definitions.
Header versioning
GET /users/123
Accept: application/vnd.myapi.v2+json
Or with a custom header:
GET /users/123
API-Version: 2
Keeps URLs clean but requires clients to set headers correctly.
Request parameter versioning
/users/123?version=2
/users/123?api_version=2024-01-01
AWS uses date-based versioning: ?version=2024-01-01. Easy for quick testing.
Implementing URL versioning in Express
const express = require('express');
const app = express();
// Version-specific routers:
const v1Router = require('./routes/v1');
const v2Router = require('./routes/v2');
app.use('/v1', v1Router);
app.use('/v2', v2Router);
// routes/v1/users.js:
router.get('/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
res.json({
firstName: user.first_name,
lastName: user.last_name,
});
});
// routes/v2/users.js:
router.get('/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
res.json({
first_name: user.first_name,
last_name: user.last_name,
full_name: `${user.first_name} ${user.last_name}`,
});
});
Response transformation approach
Instead of duplicate routes, transform responses at a versioning layer:
// Single route, multiple response transformers:
const transformers = {
v1: (user) => ({
firstName: user.first_name,
lastName: user.last_name,
}),
v2: (user) => ({
first_name: user.first_name,
last_name: user.last_name,
full_name: `${user.first_name} ${user.last_name}`,
}),
};
app.get('/v:version/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
const version = `v${req.params.version}`;
const transform = transformers[version] || transformers.v1;
res.json(transform(user));
});
Using JSON Diff to document breaking changes
When publishing a new API version, diff the response schemas:
import { compare } from 'fast-json-patch';
const v1Response = {
"firstName": "Alice",
"lastName": "Smith",
"emailAddress": "alice@example.com"
};
const v2Response = {
"first_name": "Alice",
"last_name": "Smith",
"email": "alice@example.com",
"displayName": "Alice Smith"
};
const diff = compare(v1Response, v2Response);
// [
// { op: 'remove', path: '/firstName' },
// { op: 'remove', path: '/lastName' },
// { op: 'remove', path: '/emailAddress' },
// { op: 'add', path: '/first_name', value: 'Alice' },
// { op: 'add', path: '/last_name', value: 'Smith' },
// { op: 'add', path: '/email', value: 'alice@example.com' },
// { op: 'add', path: '/displayName', value: 'Alice Smith' }
// ]
This diff is your breaking changes document.
Deprecation headers
Signal that a version will be sunset:
HTTP/1.1 200 OK
Deprecation: true
Sunset: Sat, 31 Dec 2026 23:59:59 GMT
Link: <https://api.example.com/v2/users>; rel="successor-version"
RFC 8594 defines these headers. Major API providers (Stripe, GitHub) use similar patterns.
Semantic versioning for APIs
Use semver-like rules:
- Patch (1.0.x): bug fixes, no schema changes
- Minor (1.x.0): new optional fields added (non-breaking)
- Major (x.0.0): breaking changes — requires version bump
// Changelog entry:
{
"version": "2.0.0",
"breaking": true,
"changes": [
{ "type": "renamed", "from": "firstName", "to": "first_name" },
{ "type": "removed", "field": "emailAddress", "replacement": "email" },
{ "type": "added", "field": "displayName" }
]
}
Related tools
- JSON Diff Tool — compare API responses between versions
- JSON API Response Format — consistent response structure
- JSON Schema Validation — validate response schemas
Related posts
- Comparing JSON Structurally (Not Just as Strings) — Two JSON documents can be byte-different and semantically identical. Or byte-ide…
- How to Compare JSON Objects — Deep Equality and Diff in JavaScript and Python — Comparing JSON objects with == won't work for deep equality. Here's how to deep-…
- Detecting Changes in JSON Data — Audit Logs, Diffs, and Change Tracking — Detecting what changed in a JSON document is essential for audit logs, versionin…
- JSON API Response Format — Structuring REST API Responses — A consistent JSON response format makes APIs predictable and easier to consume. …
- JSON Diff Tool — Compare Two JSON Objects and Find Differences — A JSON diff tool compares two JSON structures semantically, not textually. It fi…
Related tool
Compare two JSON objects structurally with field-by-field diff.
Written by Mian Ali Khalid. Part of the Data & Format pillar.