diff --git a/ipa/general/0116.mdx b/ipa/general/0116.mdx index 3774f12..d09d817 100644 --- a/ipa/general/0116.mdx +++ b/ipa/general/0116.mdx @@ -13,58 +13,1462 @@ change. ## Guidance -- Existing client code written against a revision **must not** be broken by - minor changes to the service -- API producers **must** consider syntax breaking changes -- API producers **must** consider semantic breaking changes -- API producers **must** not break backwards compatibility with a change without - issuing a new API version -- API producers **should** leverage extensible design where applicable -- New functionality **may** be added to existing versions, granted there are no - incompatibilities introduced + + + + +Existing client code written against a published version **must not** be broken +by minor changes to the service. + + + + + +```yaml +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: string +``` + + + A new optional response field is additive. A client that ignores unknown + fields keeps working unchanged, so the existing contract still holds. + + + + + + +```yaml +paths: + /users/{userId}: + get: + responses: + "200": + content: + application/json: + schema: + type: object + properties: + id: + type: string + # name was removed from the response +``` + + + 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. + + + + + +```yaml +# 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: string +``` + + + The revision was reviewed against the prior shape and limited to an additive, + optional parameter, so no caller's existing request becomes invalid. + + + + + + +```yaml +paths: + /orders: + get: + parameters: + - name: status + in: query + required: true # was optional in the published version + schema: + type: string +``` + + + 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. + + + + + +```yaml +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. +``` + + + The meaning of each value is fixed for the life of the version, so a client + that branches on `status` keeps interpreting responses correctly. + + + + + + +```yaml +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. +``` + + + The shape is unchanged but the meaning of `DONE` flipped, so existing logic + that treats `DONE` as 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. + + + + + +```yaml +# 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" +``` + + + The incompatible shape lives in a new version. Callers on the old version are + untouched and migrate on their own schedule. + + + + + + +```yaml +# 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 v1 +``` + + + 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. + + + + + +```yaml +components: + schemas: + Notification: + type: object + properties: + channel: + type: string + enum: [EMAIL, SMS] # open-ended set, callers tolerate new values + details: + type: object + additionalProperties: true +``` + + + Modeling `channel` as an extensible enum and `details` as an open object lets + new channels and fields land additively, without an incompatible change. + + + + + + +```yaml +components: + schemas: + Notification: + type: object + properties: + emailOnly: + type: boolean # bakes in a single channel + additionalProperties: false + required: [emailOnly] +``` + + + 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 -- Optional fields **must not** be removed from the request or response of an - existing version of an API version -- Guaranteed output fields **must not** be omitted (become nullable) -- Existing API components **must not** be renamed or removed from an existing - version of an API -- Default values provided for a field **must not** change -- Changes to path, query request headers or body content **must not** be made -- Field names **must not** change or be removed -- Field types **must not** be changed -- Status codes **must not** change -- Operation Ids **must not** be updated or deleted -- Operation Tags **must not** be updated or deleted -- HTTP Verbs **must not** be changed or deleted -- Media type **must not** be changed or deleted -- Existing resources **must not** be moved to a new URI -- Existing resources **must not** be removed without previously being marked as - sunsetting -- Existing options within enum fields **must not** be changed or removed + + + + +New required fields **must not** be added to an existing version of an API. + + + + + +```yaml +# 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] +``` + + + An added optional field leaves prior requests valid, because omitting it is + still accepted. + + + + + + +```yaml +components: + schemas: + CreateOrderRequest: + type: object + properties: + items: + type: array + couponCode: + type: string + required: [items, couponCode] # newly required +``` + + + Marking a new field required rejects every existing request that omits it. + + + + + + + For each request schema, diff the `required` array between the published + version and the revision. + + + Flag any entry present in the revision's `required` list 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. + + + + + +```yaml +components: + schemas: + UserResponse: + type: object + properties: + id: + type: string + nickname: # optional field retained + type: string +``` + + + Keeping the optional field preserves the shape callers already read and send. + + + + + + +```yaml +components: + schemas: + UserResponse: + type: object + properties: + id: + type: string + # nickname was deleted +``` + + + 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. + + + + + +```yaml +components: + schemas: + InvoiceResponse: + type: object + properties: + total: + type: number + required: [total] # always returned, never null +``` + + + A field guaranteed in the published version stays required and non-nullable, + so callers can keep reading it unconditionally. + + + + + + +```yaml +components: + schemas: + InvoiceResponse: + type: object + properties: + total: + type: number + nullable: true # was always present before +``` + + + 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. + + + + + +```yaml +components: + schemas: + UserResponse: # name unchanged across the version + type: object + properties: + id: + type: string +``` + + + Stable component names keep `$ref` targets and generated client types intact. + + + + + + +```yaml +components: + schemas: + UserPayload: # renamed from UserResponse + type: object + properties: + id: + type: string +``` + + + Renaming a component breaks every `$ref` to 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. + + + + + +```yaml +components: + schemas: + ListProjectsQuery: + type: object + properties: + pageSize: + type: integer + default: 100 # unchanged from the published version +``` + + + A stable default keeps the behavior of requests that omit the field identical + to before. + + + + + + +```yaml +components: + schemas: + ListProjectsQuery: + type: object + properties: + pageSize: + type: integer + default: 20 # was 100 +``` + + + Changing the default silently alters the result of every request that relied + on the old default. + + + + + + + For each schema property, read its `default` in the published version and + the revision. + + + Flag any property whose `default` value 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. + + + + + +```yaml +paths: + /projects/{projectId}/tasks: + post: + parameters: + - name: dryRun + in: query + required: false # additive only + schema: + type: boolean +``` + + + Only an optional query parameter is added; the path and existing body and + headers are untouched, so existing requests still succeed. + + + + + + +```yaml +paths: + /projects/{projectId}/work-items: # path renamed from /tasks + post: + requestBody: + content: + application/json: + schema: + type: object + required: [priority] # body now demands a new field +``` + + + 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. + + + + + +```yaml +components: + schemas: + Order: + type: object + properties: + createdAt: # name preserved + type: string + format: date-time +``` + + + A stable field name keeps serialization and deserialization working for + existing callers. + + + + + + +```yaml +components: + schemas: + Order: + type: object + properties: + creationTime: # renamed from createdAt + type: string + format: date-time +``` + + + 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. + + + + + +```yaml +components: + schemas: + Product: + type: object + properties: + price: + type: number # type unchanged +``` + + + Holding the declared type steady keeps the value parseable by clients + generated against the published version. + + + + + + +```yaml +components: + schemas: + Product: + type: object + properties: + price: + type: string # was number +``` + + + 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 `type` and `format`. + + + Flag any property whose `type` or `format` differs between the versions. + + Report each changed field type. + + + + + + + + +Status codes **must not** change. + + + + + +```yaml +paths: + /orders/{orderId}: + get: + responses: + "200": # still returned on success + description: OK + "404": + description: Not found +``` + + + A stable set of status codes lets callers keep branching on the same outcomes. + + + + + + +```yaml +paths: + /orders/{orderId}: + get: + responses: + "204": # success code changed from 200 + description: No content + "404": + description: Not found +``` + + + 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. + + + + + +```yaml +paths: + /users/{userId}: + get: + operationId: getUser # unchanged +``` + + + A stable `operationId` keeps generated method names and tooling references + intact. + + + + + + +```yaml +paths: + /users/{userId}: + get: + operationId: fetchUser # renamed from getUser +``` + + + Renaming an `operationId` breaks generated SDK method names and any code that + calls them. + + + + + + + Collect every `operationId` in the published version. + + + Confirm each one still exists, unchanged, on the same operation in the + revision. + + + Report any `operationId` that was renamed or deleted. + + + + + + + + + +Operation tags **must not** be updated or deleted. + + + + + +```yaml +paths: + /users/{userId}: + get: + operationId: getUser + tags: [Users] # unchanged +``` + + + Stable tags keep generated client grouping and documentation structure + consistent for callers. + + + + + + +```yaml +paths: + /users/{userId}: + get: + operationId: getUser + tags: [Accounts] # changed from Users +``` + + + Changing a tag reorganizes generated SDK namespaces and documentation that + callers navigate by. + + + + + + + For each operation, list its `tags` in 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. + + + + + +```yaml +paths: + /users/{userId}: + get: # verb preserved + operationId: getUser +``` + + + Keeping the verb on a path means existing requests reach the same operation. + + + + + + +```yaml +paths: + /users/{userId}: + post: # was get + operationId: getUser +``` + + + 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. + + + + + +```yaml +paths: + /reports/{reportId}: + get: + responses: + "200": + content: + application/json: # media type preserved + schema: + type: object +``` + + + A stable media type keeps content negotiation and client parsing working as + before. + + + + + + +```yaml +paths: + /reports/{reportId}: + get: + responses: + "200": + content: + application/xml: # was application/json + schema: + type: object +``` + + + Changing the media type breaks clients that send the old `Accept` header or + parse the old format. + + + + + + + For each request body and response, list the media type keys under `content` + in 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. + + + + + +```yaml +paths: + /projects/{projectId}/tasks/{taskId}: # path preserved + get: + operationId: getTask +``` + + + Keeping the URI stable means existing client requests still resolve to the + same resource. + + + + + + +```yaml +paths: + /tasks/{taskId}: # moved out from under /projects/{projectId} + get: + operationId: getTask +``` + + + 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. + + + + + +```yaml +paths: + /orders/{orderId}: + get: + deprecated: true + responses: + "200": + headers: + Sunset: # announced before removal + schema: + type: string + format: date-time +``` + + + The resource is marked deprecated and announces a sunset date, giving callers + a defined window to migrate before it is removed. + + + + + + +```yaml +paths: + /invoices/{invoiceId}: + # the entire path was deleted in this revision with no prior sunset notice + get: + operationId: getInvoice +``` + + + 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: + true` marking and a `Sunset` header announced ahead of removal. + + + Report any resource removed without a prior sunset announcement. + + + + + + + + + +Existing options within enum fields **must not** be changed or removed. + + + + + +```yaml +components: + schemas: + Subscription: + type: object + properties: + tier: + type: string + enum: [FREE, PRO, ENTERPRISE] # existing options retained +``` + + + Keeping existing enum values lets callers that send or branch on them keep + working. + + + + + + +```yaml +components: + schemas: + Subscription: + type: object + properties: + tier: + type: string + enum: [FREE, PREMIUM] # PRO renamed, ENTERPRISE removed +``` + + + 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/Changes to underlying logic that results in 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 (Exceptions for machine codes or formatted - strings i.e., dates) -- Response headers **may** be added to existing versions -- Changes to a resources 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 + + + + +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 behavior of a +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