Technical notes

SchemaVela

API contracts

Backward Compatibility in APIs: Fields, Enums, and Behavioral Changes

How to tell a compatible API change from a breaking one, using JSON Schema keywords and Google's AIP-180 as a published guide rather than a universal law.

A change is backward compatible when a client written to the previous contract still works against the new one, without a code change. That sentence is the goal. It is not a property you can read off a diff of two JSON files unless you also know what those clients assume. This note uses two published sources for the mechanics: JSON Schema keywords, which define validation, and Google’s AIP-180, which is compatibility guidance for Google APIs. AIP-180 says its own lists are indicative, not exhaustive. Where this desk goes beyond either document, the paragraph says so.

Three kinds of compatibility

AIP-180 distinguishes source compatibility, wire compatibility, and semantic compatibility. Source compatibility is about client code compiling and running against a newer library. Wire compatibility is about the serialized request and response still matching. Semantic compatibility is about the client still receiving what a reasonable developer would expect. The third one is the judgment call the AIP warns about.

For an HTTP JSON API, wire compatibility is the usual bar: old requests must still be accepted, and old parsers must still read the response. Semantic compatibility is where behavior changes hide. A field can keep its name and type while the server starts filling it under a different rule. No schema keyword catches that.

Fields

How required, optional, and enum are expressed is the schema vocabulary. The compatibility reading of those keywords is here.

AIP-180 says new components, including fields, may be added in the same major version, with limits: code that does not know about the new component must still be treated as before. New required fields must not be added to existing request messages or resources. A field the client sends must have a default behavior that matches the behavior from before the field existed. A field the server previously populated must continue to be populated.

Those limits line up with JSON Schema in a straightforward way, and this alignment is an editorial reading, not a sentence in either document:

  • Adding a response property is wire-compatible for a client that ignores unknown names. It is not compatible for a client whose schema sets additionalProperties to false, or for a generated client that rejects unknown fields. If your compatibility rule assumes clients ignore unknown response fields, publish that assumption.
  • Adding an optional request property is compatible when omitting it preserves the old behavior. AIP-180’s pagination warning is the pattern: if a new page size defaults to less than the previous “return everything,” old clients silently lose results. The schema can show the field as not required and still describe a break.
  • Adding a name to required on an existing request object rejects old callers who do not send it. That is a breaking change under the AIP’s “no new required fields” rule.
  • Removing or renaming a field is incompatible in the same major version. AIP-180 says renaming is equivalent to remove-and-add. The compatible sequence is to add the new name, keep the old one, and only later remove it. That sequence is a sequence for retiring an old field.
Table 1. Field changes inside one major version
Change AIP-180 When the schema reading differs
Add an optional request field, old behavior if omitted Allowed, if the default matches previous behavior The schema cannot express “matches previous behavior.” That sentence has to be written beside it.
Add a response field Generally allowed Breaks clients that reject unknown properties
Add a required request field Not allowed JSON Schema will reject the old body as soon as the name is required
Remove or rename a field Not allowed in the same major version Stop populating it only after clients have moved
Change the type of a field Incompatible A wider type array can accept old values and new ones; a narrower one will not

The AIP-180 column summarizes that guide. The third column is this desk's reading of how a JSON Schema constraint interacts with it. Neither column is a count of client failures.

Enums

AIP-180 allows new enum values in the same major version, and then narrows the advice. Values may be added freely when the enum is used only in requests. Enums used in responses or resources, where new values are expected, should document that fact, and the addition still calls for caution, because user code may not handle the new value.

JSON Schema’s enum does not contain that distinction. A response validator with a copied enum will fail when the new value appears. If clients are expected to tolerate unknown response values, the published schema should not be a closed list they are told to enforce locally, or the compatibility rule should say that local copies of the list are a snapshot, not a promise. Checks that fail before a breaking change ships includes this case: a test that freezes an enum is useful only if a new value is supposed to be a failure.

Removing an enum value is removal of a component. AIP-180 treats that as incompatible in the same major version.

Behavior the schema will not show

Semantic compatibility fails while every schema still validates. Examples of the pattern, stated as patterns rather than as incidents:

  • A filter that used to be ignored starts to exclude rows. Old clients send the same request and receive a smaller collection.
  • A field that used to be a display string becomes an identifier that clients are expected to parse. The type is still string.
  • An error that used to be a client error becomes a success with an error object in the body, or the reverse. Callers branching on the status code change behavior without a field rename.

AIP-180’s test is whether reasonable existing clients still get what they expect. If you cannot say what they expect, you do not yet have a compatibility result. You have a diff. Write the expectation down, or call the change a break and move the version.

A compatibility checklist

Review a proposed change against the current major version. “Yes” means the change needs a written exception or a new major version. This list is an editorial aid based on the sources above. It is not itself a specification.

  1. Does any existing request become invalid because a field is now required, removed, renamed, or given a narrower type?
  2. Does an omitted new request field produce a different result from the previous default, including pagination and sort order?
  3. Does a response lose a field that servers previously populated?
  4. Can a response include a new enum value that published client schemas reject?
  5. Does the status code for an existing failure change?
  6. Does the meaning of an unchanged field change in a way a reader of the old document would not expect?
  7. If the answer to any of these is yes, is the new major version, or an explicit exception, already chosen?

Unknown fields are a policy, not a default you can skip

If your rule is “clients must ignore unknown response fields,” say it in the contract and do not publish a client schema with additionalProperties: false for those objects. If your rule is “the object is closed,” then adding a field is a break, and AIP-180’s general permission to add fields does not describe your API. Pick one. The schema and the prose have to match, or the checks you run will enforce a different contract from the one you think you published.

Tags: JSON Schemacompatibility

Sources

  1. AIP-180, Backwards compatibility — Google
  2. JSON Schema Validation, Draft 2020-12, required — JSON Schema
  3. JSON Schema Core, Draft 2020-12, additionalProperties — JSON Schema
  4. OpenAPI Specification 3.1.0, Schema Object — OpenAPI Initiative