Technical notes

SchemaVela

Versioning

API Contract Versioning: How to Avoid Breaking Integrations

How to record a compatibility promise with SemVer, OpenAPI document versions, and URL or media-type signals, without pretending one of them is required by HTTP.

Versioning is how a publisher tells a client whether yesterday’s integration still works. The number is not the promise. The promise is the rule the number is attached to: which changes are allowed inside this version, and which changes require a new one. This note compares signals that already have published definitions. It does not rank them by adoption, because this desk has no measurement of that.

Declare the public API first

Semantic Versioning 2.0.0 says software that uses it must declare a public API. That API may live in the code or only in documentation, and it should be precise. Once it exists, incompatible changes increment the major number, backward-compatible additions increment the minor number, and backward-compatible fixes increment the patch number. A pre-release version, marked with a hyphen, has lower precedence than the normal version it is attached to and is not a promise of stability.

Applied to an HTTP API, that rule is only as clear as the declaration. If the public API is “the JSON body of POST /orders and the status codes documented for it,” a new optional response field can be a minor change. If an old client was generated to reject unknown fields, the same addition is incompatible for that client. The version number cannot repair an undeclared rule. Write the rule down, then pick the number. What a schema actually constrains is the field layer of that declaration. Compatible and breaking field changes is a catalog of the cases the rule has to cover.

Major version zero, in SemVer, is initial development: anything may change. Shipping a widely used HTTP API as 0.y.z while telling clients the contract is stable contradicts that section. If the contract is meant to be stable, the version that defines it is 1.0.0 or later under SemVer, or an equivalent statement in whatever scheme you publish.

Do not confuse the three OpenAPI versions

An OpenAPI document carries at least two version strings, and teams often store a third nearby.

Table 1. Three different “versions” in an HTTP API description
String What the cited document says it is What it does not say
openapi The version of the OpenAPI Specification the file uses. Tooling should read it to interpret the file. It is not the version of your API. The specification says it is not related to info.version.
info.version The version of the OpenAPI document. Required. Distinct from the specification version and from the API implementation version. The specification does not define major or minor semantics for this string.
Deployment or build id An implementation label chosen by the publisher. This row is editorial, not an OpenAPI field. A new build can ship without a contract change, and a contract change can be documented before that build exists.

The first two rows follow OpenAPI 3.1.0. The third row is a distinction this desk recommends so a release tag is not mistaken for a contract version.

If you want SemVer semantics, say that info.version uses SemVer and point to the compatibility rule. The OpenAPI specification does not add those semantics for you.

Where the signal can sit

Publishers put the signal in different places. None of these placements is mandated by the OpenAPI specification or by SemVer.

  • A path prefix such as /v1/ is visible in logs and easy to route. It is a poor fit for a change that should not force every client onto a new URL. The prefix is part of the contract only because you said the resources under it follow one compatibility rule.
  • A header or a vendor media type can select a representation without changing the path. The client must send it, and caches and intermediaries have to be considered. This note does not claim a particular header name is standard.
  • The document version alone, with a stable URL, fits additive changes. It fails clients that cannot discover the document. Something they already parse, a response field or a documentation page they are told to read, has to carry the news.

Pick one signal for a given API and define what a change to it means. Using a path prefix and a SemVer header and an info.version that all move independently, with no map between them, is how integrations break while every number still “looks right.”

Google’s AIP-180, which is guidance for Google APIs rather than a rule for every HTTP API, states the compatibility goal directly: existing client code must not be broken by a minor or patch release, and old clients must work against newer servers of the same major version. The AIP also says its lists are indicative, not a complete catalog of every change. If you adopt that goal, the major version is the boundary where an old client may be asked to move. Moving clients from one contract to the next is the procedure on that boundary.

Deprecation is part of the version

SemVer says the minor version must be incremented if public API functionality is deprecated. The practical content of that sentence, for an HTTP API, is a date or a release on which the old behavior ends, the replacement, and a place the old client can still see the warning. A changelog line that does not name the field is not that record.

Deprecation without a removal plan leaves two schemas in production forever. Deprecation with an unannounced removal is a break. The migration note treats removal as the last phase, after consumers have had a version they can run.

Versioning an event is the same problem

A webhook payload is another public message. Changing it without a signal has the same effect as changing a response body. Versioning the payload of an event covers identity, type names, and retries. The version rule should be the same rule you published for the synchronous API, or a documented exception. An exception that exists only in a sender’s memory will not be visible to the receiver.

A checklist before you publish a version scheme

  1. The public API is described in a place a client can read, including which messages and status codes it covers.
  2. The scheme states what a compatible addition is and what forces a new major version. If the scheme is SemVer, the SemVer rules are the ones you mean, including the meaning of 0.y.z.
  3. openapi, info.version, and any deploy id are labeled as different strings.
  4. One client-visible signal is the signal. Other numbers are mapped to it or omitted.
  5. Deprecations name the element, the replacement, and the version or date after which the old element can disappear.
  6. Event payloads are either inside the same rule or called out as a separate one.

What this note does not decide

It does not tell you to put v1 in the path. It does not claim that SemVer is the only workable scheme. A date-based version, or a single integer that only increases when a break is published, can be honest if the compatibility rule is explicit. The documents compared here still leave a gap when a number moves and no compatibility rule is attached to it. Fix the rule first.

Tags: OpenAPIcompatibility

Sources

  1. Semantic Versioning 2.0.0 — SemVer
  2. OpenAPI Specification 3.1.0, OpenAPI Object and Info Object — OpenAPI Initiative
  3. AIP-180, Backwards compatibility — Google