X Xerobit

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...

Mian Ali Khalid · · 4 min read
Use the tool
JSON Diff
Compare two JSON objects structurally with field-by-field diff.
Open JSON Diff →

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 (firstNamefirst_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 posts

Related tool

JSON Diff

Compare two JSON objects structurally with field-by-field diff.

Written by Mian Ali Khalid. Part of the Data & Format pillar.