Skip to content

Latest commit

 

History

History
322 lines (249 loc) · 13.7 KB

compatibility.adoc

File metadata and controls

322 lines (249 loc) · 13.7 KB

REST Design - Compatibility

{MUST} not break backward compatibility

Change APIs, but keep all consumers running. Consumers usually have independent release lifecycles, focus on stability, and avoid changes that do not provide additional value. APIs are contracts between service providers and service consumers that cannot be broken via unilateral decisions.

There are two techniques to change APIs without breaking them:

  • follow rules for compatible extensions

  • introduce new API versions and still support older versions with deprecation

We strongly encourage using compatible API extensions and discourage versioning (see {SHOULD} avoid versioning and {MUST} use media type versioning below). The following guidelines for service providers ({SHOULD} prefer compatible extensions) and consumers ({MUST} prepare clients to accept compatible API extensions) enable us (having Postel’s Law in mind) to make compatible changes without versioning.

Note: There is a difference between incompatible and breaking changes. Incompatible changes are changes that are not covered by the compatibility rules below. Breaking changes are incompatible changes deployed into operation, and thereby breaking running API consumers. Usually, incompatible changes are breaking changes when deployed into operation. However, in specific controlled situations it is possible to deploy incompatible changes in a non-breaking way, if no API consumer is using the affected API aspects (see also Deprecation guidelines).

Hint: Please note that the compatibility guarantees are for the "on the wire" format. Binary or source compatibility of code generated from an API definition is not covered by these rules. If client implementations update their generation process to a new version of the API definition, it has to be expected that code changes are necessary.

{SHOULD} prefer compatible extensions

API designers should apply the following rules to evolve RESTful APIs for services in a backward-compatible way:

  • Add only optional, never mandatory fields.

  • Never change the semantic of fields (e.g. changing the semantic from customer-number to customer-id, as both are different unique customer keys)

  • Input fields may have (complex) constraints being validated via server-side business logic. Never change the validation logic to be more restrictive and make sure that all constraints are clearly defined in description.

  • Enum ranges can be reduced when used as input parameters, only if the server is ready to accept and handle old range values too. Enum range can be reduced when used as output parameters.

  • Enum ranges cannot be extended when used for output parameters — clients may not be prepared to handle it. However, enum ranges can be extended when used for input parameters.

  • Use {x-extensible-enum}, if range is used for output parameters and likely to be extended with growing functionality. It defines an open list of explicit values and clients must be agnostic to new values.

  • Consider [250] in case a URL has to change.

{SHOULD} design APIs conservatively

Designers of service provider APIs should be conservative and accurate in what they accept from clients:

  • Unknown input fields in payload or URL should not be ignored; servers should provide error feedback to clients via an HTTP 400 response code.

  • Be accurate in defining input data constraints (like formats, ranges, lengths etc.) — and check constraints and return dedicated error information in case of violations.

  • Prefer being more specific and restrictive (if compliant to functional requirements), e.g. by defining length range of strings. It may simplify implementation while providing freedom for further evolution as compatible extensions.

Not ignoring unknown input fields is a specific deviation from Postel’s Law (e.g. see also
The Robustness Principle Reconsidered) and a strong recommendation. Servers might want to take different approach but should be aware of the following problems and be explicit in what is supported:

  • Ignoring unknown input fields is actually not an option for {PUT}, since it becomes asymmetric with subsequent {GET} response and HTTP is clear about the {PUT} replace semantics and default roundtrip expectations (see {RFC-9110}#section-9.3.4[RFC 9110 Section 9.3.4]). Note, accepting (i.e. not ignoring) unknown input fields and returning it in subsequent {GET} responses is a different situation and compliant to {PUT} semantics.

  • Certain client errors cannot be recognized by servers, e.g. attribute name typing errors will be ignored without server error feedback. The server cannot differentiate between the client intentionally providing an additional field versus the client sending a mistakenly named field, when the client’s actual intent was to provide an optional input field.

  • Future extensions of the input data structure might be in conflict with already ignored fields and, hence, will not be compatible, i.e. break clients that already use this field but with different type.

In specific situations, where a (known) input field is not needed anymore, it either can stay in the API definition with "not used anymore" description or can be removed from the API definition as long as the server ignores this specific parameter.

{MUST} prepare clients to accept compatible API extensions

Service clients should apply the robustness principle:

  • Be conservative with API requests and data passed as input, e.g. avoid to exploit definition deficits like passing megabytes of strings with unspecified maximum length.

  • Be tolerant in processing and reading data of API responses, more specifically service clients must be prepared for compatible API extensions of response data:

    • Be tolerant with unknown fields in the payload (see also Fowler’s "TolerantReader" post), i.e. ignore new fields but do not eliminate them from payload if needed for subsequent {PUT} requests.

    • Be prepared that {x-extensible-enum} return parameter may deliver new values; either be agnostic or provide default behavior for unknown values.

    • Be prepared to handle HTTP status codes not explicitly specified in endpoint definitions. Note also, that status codes are extensible. Default handling is how you would treat the corresponding {x00} code (see {RFC-9110}#section-15[RFC 9110 Section 15]).

    • Follow the redirect when the server returns HTTP status code {301} (Moved Permanently).

{MUST} treat OpenAPI specification as open for extension by default

The OpenAPI specification is not very specific on default extensibility of objects, and redefines JSON-Schema keywords related to extensibility, like additionalProperties. Following our compatibility guidelines, OpenAPI object definitions are considered open for extension by default as per Section 5.18 "additionalProperties" of JSON-Schema.

When it comes to OpenAPI, this means an additionalProperties declaration is not required to make an object definition extensible:

  • API clients consuming data must not assume that objects are closed for extension in the absence of an additionalProperties declaration and must ignore fields sent by the server they cannot process. This allows API servers to evolve their data formats.

  • For API servers receiving unexpected data, the situation is slightly different. Instead of ignoring fields, servers may reject requests whose entities contain undefined fields in order to signal to clients that those fields would not be stored on behalf of the client. API designers must document clearly how unexpected fields are handled for {PUT}, {POST}, and {PATCH} requests.

API formats must not declare additionalProperties to be false, as this prevents objects being extended in the future.

Note that this guideline concentrates on default extensibility and does not exclude the use of additionalProperties with a schema as a value, which might be appropriate in some circumstances, e.g. see [216].

{SHOULD} avoid versioning

When changing your RESTful APIs, do so in a compatible way and avoid generating additional API versions. Multiple versions can significantly complicate understanding, testing, maintaining, evolving, operating and releasing our systems (supplementary reading).

If changing an API can’t be done in a compatible way, then proceed in one of these three ways:

  • create a new resource (variant) in addition to the old resource variant

  • create a new service endpoint — i.e. a new application with a new API (with a new domain name)

  • create a new API version supported in parallel with the old API by the same microservice

As we discourage versioning by all means because of the manifold disadvantages, we strongly recommend to only use the first two approaches.

{MUST} use media type versioning

However, when API versioning is unavoidable, you have to design your multi-version RESTful APIs using media type versioning (see {MUST} not use URL versioning). Media type versioning is less tightly coupled since it supports content negotiation and hence reduces complexity of release management.

Version information and media type are provided together via the HTTP Content-Type header — e.g. application/x.zalando.cart+json;version=2. For incompatible changes, a new media type version for the resource is created. To generate the new representation version, consumer and producer can do content negotiation using the HTTP Content-Type and Accept headers.

Note
This versioning method only applies to the request and response payload schema, not to URI or method semantics.

Custom media type format

Custom media type format should have the following pattern:

application/x.<custom-media-type>+json;version=<version>
  • <custom-media-type> is a custom type name, e.g. x.zalando.cart

  • <version> is a (sequence) number, e.g. 2

Example

In this example, a client wants only the new version of the response:

Accept: application/x.zalando.cart+json;version=2

A server responding to this, as well as a client sending a request with content should use the Content-Type header, declaring that one is sending the new version:

Content-Type: application/x.zalando.cart+json;version=2

Media type versioning should…​

  • Use a custom media type, e.g. application/x.zalando.cart+json

  • Include media type versions in request and response headers to increase visibility

  • Include Content-Type in the Vary header to enable proxy caches to differ between versions

Vary: Content-Type
Note
Until an incompatible change is necessary, it is recommended to stay with the standard application/json media type without versioning.

Further reading: API Versioning Has No "Right Way" provides an overview on different versioning approaches to handle breaking changes without being opinionated.

{MUST} not use URL versioning

With URL versioning a (major) version number is included in the path, e.g. /v1/customers. The consumer has to wait until the provider has been released and deployed. If the consumer also supports hypermedia links — even in their APIs — to drive workflows (HATEOAS), this quickly becomes complex. So does coordinating version upgrades — especially with hyperlinked service dependencies — when using URL versioning. To avoid this tighter coupling and complexer release management we do not use URL versioning, instead we {MUST} use media type versioning with content negotiation.

{MUST} always return JSON objects as top-level data structures

In a response body, you must always return a JSON object (and not e.g. an array) as a top level data structure to support future extensibility. JSON objects support compatible extension by additional attributes. This allows you to easily extend your response and e.g. add pagination later, without breaking backwards compatibility. See [161] for an example.

Maps (see [216]), even though technically objects, are also forbidden as top level data structures, since they don’t support compatible, future extensions.

{SHOULD} used open-ended list of values (x-extensible-enum) for enumerations

Enumerations are per definition closed sets of values that are assumed to be complete and not intended for extension. This closed principle of enumerations imposes compatibility issues when an enumeration must be extended. To avoid these issues, we strongly recommend to use an open-ended list of values instead of an enumeration unless:

  1. the API has full control of the enumeration values, i.e. the list of values does not depend on any external tool or interface, and

  2. the list of values is complete with respect to any thinkable and unthinkable future feature.

To specify an open-ended list of values use the marker {x-extensible-enum} as follows:

delivery_methods:
  type: string
  x-extensible-enum:
    - PARCEL
    - LETTER
    - EMAIL

Note: {x-extensible-enum} is not JSON Schema conform but will be ignored by most tools.

See [240] about enum value naming conventions.

Important: Clients must be prepared for extensions of enums returned with server responses, i.e. must implement a fallback / default behavior to handle unknown new enum values — see {MUST} prepare clients to accept compatible API extensions. API owners must take care to extend enums in a compatible way that does not change the semantics of already existing enum values, for instance, do not split an old enum value into two new enum values.