IPA-116: Backwards Compatibility
APIs are fundamentally contracts with users, and users often write code against APIs that are then launched into a production service with the expectation that it continues to work. Therefore, it is important to understand what constitutes a backward-compatible change and what constitutes a backward-incompatible change.
Guidance
Existing client code written against a published version must not be broken by minor changes to the service.
paths:
/users/{userId}:
get:
responses:
"200":
content:
application/json:
schema:
type: object
properties:
id:
type: string
name:
type: string
nickname: # newly added optional field
type: stringWhy:A new optional response field is additive. A client that ignores unknown fields keeps working unchanged, so the existing contract still holds.
paths:
/users/{userId}:
get:
responses:
"200":
content:
application/json:
schema:
type: object
properties:
id:
type: string
# name was removed from the responseWhy:Removing a field a client already reads breaks code that depends on it, even though the change looks small from the producer side.
Take the previously published version of the spec and the proposed new revision of the same version.
Diff the two: for every operation, compare request and response schemas, parameters, status codes, and media types.
Classify each difference as additive (new optional element) or subtractive/altering (removed, renamed, retyped, newly required).
Report any subtractive or altering change as a break of the existing contract.
An API producer must consider syntax breaking changes before publishing a revision.
# Two versions side by side; the v2 path adds an optional query parameter only.
paths:
/orders:
get:
parameters:
- name: status
in: query
required: false
schema:
type: stringWhy:The revision was reviewed against the prior shape and limited to an additive, optional parameter, so no caller's existing request becomes invalid.
paths:
/orders:
get:
parameters:
- name: status
in: query
required: true # was optional in the published version
schema:
type: stringWhy:The revision shipped without comparing against the published shape. Promoting an existing parameter to required rejects requests that omit it.
Locate the previously published version and the proposed revision of the same major version.
For each operation, inspect the wire-level shape: parameters, request body schema, response schema, status codes, media types, and required flags.
Confirm a documented comparison exists and that every shape difference was classified as breaking or non-breaking.
Report any revision that altered the wire shape without that comparison having been done.
An API producer must consider semantic breaking changes before publishing a revision, even when the wire shape is unchanged.
paths:
/jobs/{jobId}:
get:
responses:
"200":
content:
application/json:
schema:
type: object
properties:
status:
type: string
enum: [PENDING, RUNNING, DONE, FAILED]
description: >-
Lifecycle state. The set of values and the meaning of each
value is stable across this version.Why:The meaning of each value is fixed for the life of the version, so a client that branches on
statuskeeps interpreting responses correctly.paths:
/jobs/{jobId}:
get:
responses:
"200":
content:
application/json:
schema:
type: object
properties:
status:
type: string
enum: [PENDING, RUNNING, DONE, FAILED]
description: >-
DONE now means "accepted for processing" rather than
"completed". The shape is identical to the prior version.Why:The shape is unchanged but the meaning of
DONEflipped, so existing logic that treatsDONEas finished now misbehaves. Detecting this requires understanding server behavior, not just the spec.Identify operations whose wire shape is unchanged between the published version and the revision.
For each, read the implementation and changelog to find changed behavior: altered field semantics, changed defaults applied server-side, reordered or filtered results, changed side effects.
Assess whether existing client logic that relied on the prior behavior would now produce wrong outcomes.
Report any behavioral change that alters the meaning of a response without a corresponding new version.
Backwards compatibility must not be broken by a change without issuing a new API version.
# A breaking rename is delivered under a new version, leaving the old one intact.
paths:
/v1/users/{userId}:
get:
responses:
"200":
content:
application/json:
schema:
$ref: "#/components/schemas/UserV1"
/v2/users/{userId}:
get:
responses:
"200":
content:
application/json:
schema:
$ref: "#/components/schemas/UserV2"Why:The incompatible shape lives in a new version. Callers on the old version are untouched and migrate on their own schedule.
# The published v1 response shape is changed in place.
paths:
/v1/users/{userId}:
get:
responses:
"200":
content:
application/json:
schema:
$ref: "#/components/schemas/UserV2" # breaking change in v1Why:An incompatible change was applied to a published version. Every caller breaks at once with no opt-in and no migration path.
Diff each published version against its proposed revision and collect every breaking difference.
For each breaking difference, confirm it is introduced under a new version identifier and that the prior version retains its old behavior.
Report any breaking change applied in place to an already published version.
An API should leverage extensible design where applicable, so that future additions do not force a breaking change.
components:
schemas:
Notification:
type: object
properties:
channel:
type: string
enum: [EMAIL, SMS] # open-ended set, callers tolerate new values
details:
type: object
additionalProperties: trueWhy:Modeling
channelas an extensible enum anddetailsas an open object lets new channels and fields land additively, without an incompatible change.components:
schemas:
Notification:
type: object
properties:
emailOnly:
type: boolean # bakes in a single channel
additionalProperties: false
required: [emailOnly]Why:A boolean and a closed object hard-code today's behavior. Supporting another channel later requires removing or reshaping a required field, which is breaking.
Identify schema points where future growth is likely: status and type fields, mode flags, and containers for variable detail.
Check whether those points are modeled extensibly: enums that callers can tolerate adding to, objects rather than booleans for concepts that may grow, and nested objects for groups of related fields.
Flag designs where a foreseeable addition would require a breaking change because the current model is closed.
New functionality may be added to existing versions, provided no incompatibilities are introduced.
Syntax Breaking Changes
Changes considered breaking include:
New required fields must not be added to an existing version of an API.
# Added field is optional in the new revision.
components:
schemas:
CreateOrderRequest:
type: object
properties:
items:
type: array
couponCode: # added, not required
type: string
required: [items]Why:An added optional field leaves prior requests valid, because omitting it is still accepted.
components:
schemas:
CreateOrderRequest:
type: object
properties:
items:
type: array
couponCode:
type: string
required: [items, couponCode] # newly requiredWhy:Marking a new field required rejects every existing request that omits it.
For each request schema, diff the
requiredarray between the published version and the revision.Flag any entry present in the revision's
requiredlist but not in the published one.Report each newly required request field as a breaking change.
Optional fields must not be removed from the request or response of an existing API version.
components:
schemas:
UserResponse:
type: object
properties:
id:
type: string
nickname: # optional field retained
type: stringWhy:Keeping the optional field preserves the shape callers already read and send.
components:
schemas:
UserResponse:
type: object
properties:
id:
type: string
# nickname was deletedWhy:Deleting an optional field breaks callers that read it from responses or set it in requests.
For each request and response schema, list its declared properties in the published version and in the revision.
Flag any property present in the published version but absent from the revision.
Report each removed property as a breaking change.
Guaranteed output fields must not be omitted or made nullable in an existing version.
components:
schemas:
InvoiceResponse:
type: object
properties:
total:
type: number
required: [total] # always returned, never nullWhy:A field guaranteed in the published version stays required and non-nullable, so callers can keep reading it unconditionally.
components:
schemas:
InvoiceResponse:
type: object
properties:
total:
type: number
nullable: true # was always present beforeWhy:Allowing a previously guaranteed field to be null forces callers to handle a case that never used to occur, breaking code that assumed a value.
For each response schema, identify fields that were guaranteed in the published version (present and non-nullable, typically in
required).In the revision, confirm each such field is still present, required, and not marked
nullable.Report any guaranteed output field that became optional, nullable, or absent.
Existing API components must not be renamed or removed from an existing version of an API.
components:
schemas:
UserResponse: # name unchanged across the version
type: object
properties:
id:
type: stringWhy:Stable component names keep
$reftargets and generated client types intact.components:
schemas:
UserPayload: # renamed from UserResponse
type: object
properties:
id:
type: stringWhy:Renaming a component breaks every
$refto the old name and the generated types clients compiled against.Enumerate the keys under
components(schemas, parameters, responses, and so on) in the published version.Confirm each key still exists under the same name in the revision.
Report any component key that was renamed or removed.
Default values provided for a field must not change.
components:
schemas:
ListProjectsQuery:
type: object
properties:
pageSize:
type: integer
default: 100 # unchanged from the published versionWhy:A stable default keeps the behavior of requests that omit the field identical to before.
components:
schemas:
ListProjectsQuery:
type: object
properties:
pageSize:
type: integer
default: 20 # was 100Why:Changing the default silently alters the result of every request that relied on the old default.
For each schema property, read its
defaultin the published version and the revision.Flag any property whose
defaultvalue differs between the two versions.Report each changed default as a breaking change.
Changes to path, query, request headers, or body content must not be made to an existing version.
paths:
/projects/{projectId}/tasks:
post:
parameters:
- name: dryRun
in: query
required: false # additive only
schema:
type: booleanWhy:Only an optional query parameter is added; the path and existing body and headers are untouched, so existing requests still succeed.
paths:
/projects/{projectId}/work-items: # path renamed from /tasks
post:
requestBody:
content:
application/json:
schema:
type: object
required: [priority] # body now demands a new fieldWhy:Renaming the path and demanding a new body field invalidates requests written against the published version.
For each operation, compare the path template, query parameters, request headers, and request body schema between versions.
Classify each difference as additive-and-optional or as an alteration to an existing element.
Report any non-additive change to path, query, request headers, or body.
Field names must not change or be removed.
components:
schemas:
Order:
type: object
properties:
createdAt: # name preserved
type: string
format: date-timeWhy:A stable field name keeps serialization and deserialization working for existing callers.
components:
schemas:
Order:
type: object
properties:
creationTime: # renamed from createdAt
type: string
format: date-timeWhy:Renaming a field means existing callers send and read a name the server no longer recognizes.
For each schema, list its property names in the published version and the revision.
Flag any name that disappeared or whose value moved under a differently named property.
- Report each renamed or removed field.
Field types must not be changed.
components:
schemas:
Product:
type: object
properties:
price:
type: number # type unchangedWhy:Holding the declared type steady keeps the value parseable by clients generated against the published version.
components:
schemas:
Product:
type: object
properties:
price:
type: string # was numberWhy:Changing the type breaks typed clients and any caller that parsed the value as the original type.
For each property present in both versions, read its
typeandformat.Flag any property whose
typeorformatdiffers between the versions.- Report each changed field type.
Status codes must not change.
paths:
/orders/{orderId}:
get:
responses:
"200": # still returned on success
description: OK
"404":
description: Not foundWhy:A stable set of status codes lets callers keep branching on the same outcomes.
paths:
/orders/{orderId}:
get:
responses:
"204": # success code changed from 200
description: No content
"404":
description: Not foundWhy:Changing the success status code breaks callers that check for the original code.
For each operation, list the response status codes declared in the published version and the revision.
Flag any code that was removed or whose meaning was reassigned (a former success code dropped in favor of another).
- Report each altered or removed status code.
Operation ids must not be updated or deleted.
paths:
/users/{userId}:
get:
operationId: getUser # unchangedWhy:A stable
operationIdkeeps generated method names and tooling references intact.paths:
/users/{userId}:
get:
operationId: fetchUser # renamed from getUserWhy:Renaming an
operationIdbreaks generated SDK method names and any code that calls them.Collect every
operationIdin the published version.Confirm each one still exists, unchanged, on the same operation in the revision.
Report any
operationIdthat was renamed or deleted.
Operation tags must not be updated or deleted.
paths:
/users/{userId}:
get:
operationId: getUser
tags: [Users] # unchangedWhy:Stable tags keep generated client grouping and documentation structure consistent for callers.
paths:
/users/{userId}:
get:
operationId: getUser
tags: [Accounts] # changed from UsersWhy:Changing a tag reorganizes generated SDK namespaces and documentation that callers navigate by.
For each operation, list its
tagsin the published version and the revision.Flag any tag that was renamed or removed from an existing operation.
- Report each altered or deleted operation tag.
HTTP verbs must not be changed or deleted.
paths:
/users/{userId}:
get: # verb preserved
operationId: getUserWhy:Keeping the verb on a path means existing requests reach the same operation.
paths:
/users/{userId}:
post: # was get
operationId: getUserWhy:Moving an operation to a different verb makes every request that used the old verb fail to route.
For each path, list the HTTP methods defined in the published version.
Confirm each method still exists on the same path in the revision.
Report any verb that was removed or whose operation moved to a different verb.
Media types must not be changed or deleted.
paths:
/reports/{reportId}:
get:
responses:
"200":
content:
application/json: # media type preserved
schema:
type: objectWhy:A stable media type keeps content negotiation and client parsing working as before.
paths:
/reports/{reportId}:
get:
responses:
"200":
content:
application/xml: # was application/json
schema:
type: objectWhy:Changing the media type breaks clients that send the old
Acceptheader or parse the old format.For each request body and response, list the media type keys under
contentin the published version.Confirm each media type still exists in the revision.
Report any media type that was changed or removed.
Existing resources must not be moved to a new URI.
paths:
/projects/{projectId}/tasks/{taskId}: # path preserved
get:
operationId: getTaskWhy:Keeping the URI stable means existing client requests still resolve to the same resource.
paths:
/tasks/{taskId}: # moved out from under /projects/{projectId}
get:
operationId: getTaskWhy:Moving a resource to a new URI makes every request to the old path return a not-found error.
Enumerate every path template in the published version.
Confirm each path still exists in the revision (matching the same operation by
operationId).Report any operation whose path template changed.
Existing resources must not be removed without previously being marked as sunsetting.
paths:
/orders/{orderId}:
get:
deprecated: true
responses:
"200":
headers:
Sunset: # announced before removal
schema:
type: string
format: date-timeWhy:The resource is marked deprecated and announces a sunset date, giving callers a defined window to migrate before it is removed.
paths:
/invoices/{invoiceId}:
# the entire path was deleted in this revision with no prior sunset notice
get:
operationId: getInvoiceWhy:Deleting a live resource that was never marked sunsetting breaks callers with no warning and no migration window.
Identify paths present in the published version but absent from the revision.
For each removed path, check the prior version's history for a
deprecated: truemarking and aSunsetheader announced ahead of removal.Report any resource removed without a prior sunset announcement.
Existing options within enum fields must not be changed or removed.
components:
schemas:
Subscription:
type: object
properties:
tier:
type: string
enum: [FREE, PRO, ENTERPRISE] # existing options retainedWhy:Keeping existing enum values lets callers that send or branch on them keep working.
components:
schemas:
Subscription:
type: object
properties:
tier:
type: string
enum: [FREE, PREMIUM] # PRO renamed, ENTERPRISE removedWhy:Renaming or dropping an enum value rejects requests carrying the old value and breaks callers that match on it in responses.
For each enum field, list its values in the published version and the revision.
Flag any value present in the published version but missing from the revision (removed or renamed).
Report each removed or changed enum option. New options added alongside the existing ones are allowed.
Changes considered non-breaking include:
New output fields may be added to existing versions.
Optional input parameters may be added to existing versions.
Updates to underlying logic that produce different results without affecting the shape of the response may be added to existing versions.
Changes to unstructured, human-readable string values may be added to existing versions, except for machine codes or formatted strings such as dates.
Response headers may be added to existing versions.
Changes to a resource's authorization may be added to existing versions.
New options may be added to existing enums.
Input and output fields may be marked as deprecated in existing versions.
Semantic Breaking Changes
Semantic-based breaking changes capture situations in which the behavior of a resource is changing without necessarily including a syntax-based breaking change that would produce a top-level error. These situations often require intensive data analysis and discovery to fully understand the extent of customer impact for the change.