Breaking vs Non-Breaking Changes
| Change | Impact | Example |
|---|---|---|
| Add optional field | Non-breaking | + "avatar_url": "https://..." |
| Add required field | Breaking for consumers that validate strictly | + "tenant_id": "..." (required) |
| Remove field | Breaking | - "legacy_id" removed |
| Rename field | Breaking | "name" → "full_name" |
| Change type | Breaking | "age": "30" → "age": 30 |
| Nest flat field | Breaking | "city" → "address": {"city": "..."} |
| Change enum values | Breaking | "status": "active" → "enabled" |
| Add enum value | Potentially breaking | "role": "viewer" (new value) |
API Versioning Strategies
1. URL Path Versioning (Most Common)
1GET /v1/users/42 → {"name": "Alice", "email": "[email protected]"}2GET /v2/users/42 → {"fullName": "Alice Chen", "email": "[email protected]", "role": "admin"}Tip
URL path versioning is the most developer-friendly approach. It is visible, cacheable, and easy to test with curl. Used by GitHub, Stripe, Twilio, and most major APIs.
2. Header Versioning
1GET /users/422Accept: application/vnd.myapi.v2+json34# Or custom header5GET /users/426X-API-Version: 23. Date-Based Versioning (Stripe Pattern)
1GET /users/422Stripe-Version: 2026-04-0234# API key is pinned to a version at creation time5# Requests without explicit version use the pinned version| Strategy | Pros | Cons | Used By |
|---|---|---|---|
| URL path (/v2/) | Visible, cacheable, simple | Duplicates routes | GitHub, Twilio |
| Header | Clean URLs, flexible | Harder to test, not cacheable | Azure, some internal APIs |
| Date-based | Granular, automatic pinning | Complex to implement | Stripe |
| Query param (?v=2) | Easy to add | Pollutes URLs, cache issues | Google Maps (legacy) |
Migration Patterns
1. Parallel Fields (Transition Period)
Keep both old and new field names during a transition window:
v1.5 — transitional responsejson
1{2 "name": "Alice Chen",3 "fullName": "Alice Chen",4 "email": "[email protected]",5 "_deprecations": {6 "name": "Use 'fullName' instead. 'name' will be removed on 2026-09-01."7 }8}2. Response Transformer
Version-aware serializertypescript
1interface User {2 id: number;3 fullName: string;4 email: string;5 role: string;6}78function serializeUser(user: User, version: number) {9 if (version === 1) {10 return {11 id: user.id,12 name: user.fullName, // v1 clients expect "name"13 email: user.email,14 };15 }1617 return {18 id: user.id,19 fullName: user.fullName,20 email: user.email,21 role: user.role,22 };23}3. JSON Migration Script
Data migration from v1 to v2typescript
1interface V1User {2 name: string;3 email: string;4 address: string; // flat string in v15}67interface V2User {8 fullName: string;9 email: string;10 address: { // structured object in v211 street: string;12 city: string;13 country: string;14 };15}1617function migrateV1ToV2(v1: V1User): V2User {18 const parts = v1.address.split(', ');19 return {20 fullName: v1.name,21 email: v1.email,22 address: {23 street: parts[0] ?? '',24 city: parts[1] ?? '',25 country: parts[2] ?? '',26 },27 };28}JSON Schema Composition for Versioning
v1.schema.jsonjson
1{2 "$id": "https://api.example.com/schemas/user/v1",3 "type": "object",4 "required": ["id", "name", "email"],5 "properties": {6 "id": { "type": "integer" },7 "name": { "type": "string" },8 "email": { "type": "string", "format": "email" }9 }10}v2.schema.json — extends v1json
1{2 "$id": "https://api.example.com/schemas/user/v2",3 "allOf": [4 { "$ref": "v1.schema.json" },5 {6 "required": ["role"],7 "properties": {8 "fullName": { "type": "string" },9 "role": { "type": "string", "enum": ["admin", "editor", "viewer"] },10 "avatar_url": { "type": "string", "format": "uri" }11 }12 }13 ]14}Deprecation Lifecycle
Deprecation headerstext
1HTTP/1.1 200 OK2Content-Type: application/json3Deprecation: true4Sunset: Sat, 01 Sep 2026 00:00:00 GMT5Link: <https://api.example.com/v2/users>; rel="successor-version"67{"name": "Alice", ...}Best Practices
- ✓Default to additive changes — add new fields, never remove or rename
- ✓Use contract testing to detect breaking changes automatically in CI
- ✓Provide a migration guide with every version bump
- ✓Include deprecation headers (Sunset, Deprecation) on old versions
- ✓Support at least 2 versions simultaneously during transition
- ✓Log which clients are still using deprecated versions
- ✗Don't rename fields — add the new name and deprecate the old
- ✗Don't change field types without a new version
Try It — Validate a Versioned Response
Try It Yourself
A transitional response with both old and new field names
Try These Tools
Continue Learning
Frequently Asked Questions
What is a breaking change in a JSON API?
A breaking change is any modification that causes existing clients to fail: removing a field, changing a field type (string → number), renaming a field, changing the structure (object → array), or altering enum values. Adding new fields is generally non-breaking.
Should I version my JSON API with URL paths or headers?
URL path versioning (/v2/users) is the most common and easiest for clients. Header versioning (Accept: application/vnd.api+json;version=2) is cleaner but harder to test. Content-type versioning is most RESTful but least practical. Most teams use URL path versioning.
How do I maintain two versions of a JSON API simultaneously?
Use a versioned router that dispatches to different handlers per version. Share common business logic and only branch at the serialization layer. Add deprecation headers to older versions. Set a sunset date and communicate it clearly in documentation.
What is JSON Schema composition for versioning?
Use JSON Schema $ref, allOf, and oneOf to compose versioned schemas from shared definitions. A v2 schema can extend v1 with additional fields using allOf: [{$ref: "v1.schema.json"}, {properties: {newField: ...}}]. This keeps schemas DRY and makes the evolution visible.
How do Stripe and GitHub handle API versioning?
Stripe uses date-based versioning (2026-04-02). Each API key is pinned to a version and requests include a Stripe-Version header. GitHub uses URL path versioning but also Accept headers for preview features. Both maintain backward compatibility by defaulting to the version the client was created with.