The API schema design decision record: why the response shape you chose in year one determines your consumer migration cost and your schema evolution flexibility in year three
Response shape, enum extensibility, nested object embedding, and field deprecation policy are decided in the first API design session and never documented. When the product needs to add a new subscription state six months later, the iOS mobile app crashes because the enum was treated as exhaustive. When billing addresses need to become their own resource, 61 consumer integrations each reference the embedded object directly, making the migration cost proportional to every consumer's update cycle. The API schema design is an architectural decision. It should be documented before the first consumer ships against it.
A 27-person SaaS company had a subscription API at GET /subscriptions/{id}. The response included a status field. In the OpenAPI spec, it was typed as string with an enum annotation of ["active", "inactive"] — added when the first engineer built the endpoint and there were only two states. The spec's enum annotation listed the two values but said nothing about whether the enum was exhaustive. The decision that was made in that first session — use an OpenAPI enum annotation with the current set of values — is the kind of decision that is never written down because it feels obvious at the time. The two values are documented. What is not documented is the contract: whether the list is closed and exhaustive, meaning new values constitute a breaking change, or open and extensible, meaning consumers must handle values not in the current list.
In month eight, the payment team added a "suspended" state for subscriptions where the customer's credit card had failed. The backend change was straightforward — a new value in the database enum, new logic in the billing processor. The API change was adding "suspended" to the OpenAPI enum annotation and updating the response serializer to emit the new value. The change was reviewed, approved, deployed to production on a Tuesday morning.
At 9:47 AM, the on-call engineer received an error rate alert: the iOS mobile app's crash rate had increased from 0.02% to 6.8%. The crash reports showed an unhandled exception in the subscription status renderer: Unrecognized enum value "suspended". The iOS SDK (version 1.4.2) had been written by a contractor who had used Swift's exhaustive enum pattern — a switch statement over the status field with a case for .active, a case for .inactive, and no default case. Swift's compiler treated the missing case as unreachable; the contractor had left no documentation and the code had been in production for eight months without incident. The "suspended" value that arrived from the server produced a crash in 31% of active users whose subscriptions had entered the suspended state.
The rollback was immediate: stop emitting "suspended" in the API response, return "inactive" for suspended subscriptions. This restored app stability but created a data inconsistency: the database had a "suspended" state that the API actively hid. Users with failed payments saw "active" status in their mobile app while their subscriptions were actually suspended and their access was restricted on the server. Support tickets followed — users confused about why features were unavailable on an "active" subscription. Three escalated to the CTO. The iOS fix (adding a default case to the switch statement) took two days to write, one day to review, and eleven days to receive Apple App Store approval, during which the data inconsistency persisted.
The root cause was not the contractor's exhaustive switch. The root cause was that the API schema had never documented whether the status enum was open (extensible, consumers must handle unknown values) or closed (exhaustive, new values constitute a breaking change). Without that documented contract, the iOS developer's assumption — that an enum in an OpenAPI spec is exhaustive — was reasonable. The backend developer's assumption — that adding a new value to a string enum is not a breaking change — was also reasonable. Both assumptions were coherent in isolation. Together they produced a 31% mobile crash rate and eleven days of data inconsistency. The API versioning strategy decision record is the downstream mechanism that breaking changes make necessary — a versioning strategy determines how the product moves consumers from one schema to another — but the right schema design with a documented open enum contract would have made the "suspended" addition a non-breaking change, never triggering the versioning question in the first place.
A 34-person B2B SaaS added a billing module in year one. The account API response included an embedded billingAddress object: { "street": "...", "city": "...", "state": "...", "zip": "...", "country": "..." }. The embedding was intuitive — every account had exactly one billing address at launch, so including it in the account response saved an extra API call. The OpenAPI spec documented the embedded object's fields. The decision to embed was made in the "add billing to account API" AI session in year one. There was no record of the alternative considered: returning a billingAddressId reference and exposing a separate GET /billing-addresses/{id} endpoint. The embedding was chosen for developer convenience. The schema implications — the coupling of every account API consumer to the exact shape of the billing address object — were not analyzed. The cost of that coupling would not be visible for two years.
Two years later, the team built a multi-method payment feature for enterprise customers who needed separate billing addresses for different cost centers. A single account needed multiple billing addresses, each associated with one or more payment methods. The billingAddress object needed to become a collection — billingAddresses, an array of address objects each with an id field. This change was structurally necessary and there was no way to make it non-breaking: the field type was changing from object to array, the field name was changing, and the access pattern was changing from account.billingAddress.street to account.billingAddresses[0].street.
The migration planning session produced a count that stopped the product manager: 61 consumer integrations. The number came from scanning the internal API access logs for the billingAddress field path over the previous 90 days: 11 internal frontend components, 8 internal backend services, 4 mobile app versions (iOS 2.1, iOS 2.0, Android 2.3, Android 2.2 — still in use by customers who had not updated), 38 third-party integrations (accounting tools, ERP connectors, custom scripts in customer environments). Every one of them read account.billingAddress.street or one of the other five fields in the embedded object.
The migration required a dual-field period: the account response would emit both billingAddress (the old singular object, populated with the first address for backwards compatibility) and billingAddresses (the new array). The dual-field period was estimated at 8 months to give enterprise customers enough lead time to update their integrations. That meant maintaining both fields in the response serializer and the database migration — creating an address table, adding a foreign key, backfilling the existing embedded data — had to be designed to support both the legacy singular object query path and the new array path simultaneously for those 8 months. When the dual-field period ended and billingAddress was removed, three integrations broke because their owners had not seen the deprecation notice or had deprioritized the migration.
The API design decision — embed the address versus return a reference ID — had been made in that single AI session in year one and had never been documented. There was no record of the alternatives considered, no note about what would need to change if billing addresses became a separate resource, and no analysis of what the consumer migration cost would be if the shape changed. The migration planning session two years later was the first time anyone had calculated the 61-consumer number. The pattern is what first-year startup decisions consistently produce: a small team makes a technically reasonable choice that works well for the current scale, the product grows, and the choice's structural implications only become visible at the scale for which the choice was not evaluated.
The three structural properties that the API schema design determines
When teams design an API for the first time, the scope is narrow: define the endpoints and the response fields that the current consumer needs. The OpenAPI spec is the artifact that proves the work is done. The structural properties that the shape choices set — breaking change surface, schema evolution compatibility, and consumer migration cost — are not visible when the product has one consumer and a small user base. They become visible when the product has 61 consumers, a mobile app with a slow release cycle, and a business requirement that requires restructuring a resource that was embedded in the foundational response.
Breaking change surface. The response shape determines what future changes are possible without forcing simultaneous consumer migrations. Three specific shape decisions that expand or reduce the breaking change surface matter most at the API design stage. The first is the enum extensibility contract: an open enum with documented consumer obligations (handle unknown values gracefully) allows new values to be added without a version bump, because every consumer has explicitly agreed to implement a default case for unknown values; a closed enum guarantees exhaustiveness and allows consumers to write exhaustive switch statements, but makes any value addition a breaking change requiring a coordinated migration. The second is embedding versus referencing: embedded objects couple consumers to the embedded resource's exact shape, so any evolution of the embedded resource — new required fields, type changes, restructuring as a collection — becomes a breaking change for all consumers that read the embedded fields; ID references decouple the parent schema from the referenced resource's evolution, allowing the referenced resource to evolve at its own pace with its own versioning strategy. The third is the required-versus-optional field policy: a field that is required in the initial schema and later needs to become optional is a breaking change for consumers that depend on the field always being present; a field that is optional and later becomes required is a breaking change for consumers that omit it in requests. Each of these decisions locks in a portion of the breaking change surface before the first consumer ships. None of them are visible in a code review that only asks "does this return the right data."
Schema evolution compatibility rules. The API schema design determines the rules by which future changes are evaluated as breaking or non-breaking. Different consumer patterns treat the same change differently: a statically-typed consumer treats adding a new optional field as non-breaking and accepts the new field without change; a consumer that validates response bodies against a locally-maintained schema with additionalProperties: false treats the same addition as breaking, because the local schema now rejects a field the server added. A consumer that writes an exhaustive switch on a status field treats adding a new enum value as breaking; a consumer that logs the field value and continues treats the same addition as non-breaking. Without a documented taxonomy of what constitutes a breaking change under the team's specific schema and consumer obligations, two engineers on the same team will make different judgments about whether a PR requires a version bump — and both will be correct under their implicit model of the API contract. The test strategy decision record intersects here: consumer contract tests using Pact or Spring Cloud Contract verify that the producer schema satisfies the contracts registered by existing consumers before deployment, making breaking change detection an automated CI gate rather than a manual engineering judgment in each PR review.
Consumer migration cost. A field used by N consumers has a removal or migration cost proportional to N multiplied by each consumer's update cycle. Mobile app release cycles — two to four weeks of development plus app store review time — are fundamentally different from internal backend service cycles of days or hours. An enterprise customer's custom integration script may have an update cycle measured in months if the customer's engineering team is small and the API integration is not their highest priority. The 61-consumer count and the 8-month dual-field period are not accidents of bad planning; they are the structural consequence of an embedding decision made without documenting the consumer migration cost implications. A deprecation policy that specifies minimum notice periods per consumer tier — 6 months for external third-party integrations, 3 months for mobile apps accounting for app store review cycles, 4 weeks for internal services — makes the migration timeline predictable and coordinated. Without a documented policy, each deprecation is negotiated from scratch, and the negotiation consistently favors longer timelines because there is no written commitment that consumers can hold the API team to. The quarterly decision review process is where the API schema deprecation decisions should be surfaced as consumer count grows: a field that had 3 consumers at last review now has 61, and the migration cost has changed by a factor of 20.
API schema design options and their structural properties
Undocumented ad-hoc JSON — no schema, no versioning, response shape defined only by the serializer output — is the path of least initial friction and is correct only for single-consumer internal services where the producer and consumer are owned by the same team, deployed and updated simultaneously, and where the consumer has no integration stability guarantee. The moment a second team consumes the API, or a mobile app ships against it, or a third-party integration is built on top of it, the absence of a schema becomes the absence of a contract: no mechanism exists to detect breaking changes before deployment, no consumer has written evidence of what they can depend on, and the producer's ability to evolve is bounded only by what the producer can discover from scanning consumer code. Teams that start with undocumented JSON consistently discover the cost of this choice during their first breaking change incident, at which point retrofitting a schema requires reverse-engineering the implicit contract from the existing consumers — work that is proportional to the number of consumers and the number of fields they use.
OpenAPI 3.x with explicit enum values, required/optional annotations, and schema comment extensibility contracts is the standard approach for REST APIs with external consumers. The OpenAPI specification is the schema contract: it documents each field's type, whether it is required or optional, the current enum values for enumerated fields, and — when the team follows the conventions discussed in this ADR — the extensibility contract for each enum field in the description. Breaking change detection is automated via openapi-diff in CI: a PR that removes a required field, changes a field type, or removes an enum value fails the diff check before merge. The limitation is that OpenAPI 3.x has no first-class mechanism for distinguishing open enums from closed enums; the convention is a field description that states explicitly whether new values may be added without a breaking change notice, and a note in the API changelog policy. OpenAPI 3.0 and 3.1 also have a semantic ambiguity between nullable and optional that affects how consumers handle absent versus null field values — a field marked nullable: true in OpenAPI 3.0 means the field may be present with a null value but does not mean the field may be absent; required: false means the field may be absent. This distinction matters for statically-typed consumers who generate code from the spec: a field that is both nullable and optional produces different generated types in different code generators, and the correct handling of absent-versus-null is a contract detail that should be documented explicitly in the ADR for each field that may be either. The build-versus-buy decision applies to OpenAPI code generator tooling: using oapi-codegen or openapi-generator to generate server stubs and client SDKs from the spec enforces that the implementation matches the spec, eliminating the class of drift bugs where the serializer emits a field the spec does not document; the alternative of hand-rolling the serializer and maintaining the spec separately produces divergence over time as the implementation evolves and the spec update is skipped.
Protobuf and gRPC use field numbers as the wire identity for fields, not field names. A field rename in Protobuf does not break wire compatibility, because the serialized binary data identifies fields by number; the name is only a developer convenience. A removed field should use the reserved keyword to prevent the field number from being reused for a different field in the future — reusing a field number would cause clients that still send the old field to silently populate the new field with incompatible data. Enum value 0 in proto3 is the default value for any unset enum field, which gives enum extensibility a natural implementation: consumers that receive an unknown enum value get 0 (the default) rather than a crash. Proto3 treats all fields as implicitly optional by default, which means "required field" semantics must be implemented in application logic rather than the Protobuf schema, with corresponding documentation in the ADR. Protobuf is the correct choice for internal service-to-service APIs where both the producer and all consumers are updated within the same deployment pipeline, where binary protocol efficiency matters at high throughput, and where the team can invest in the schema evolution discipline that buf lint and buf breaking CI checks enforce. The CI/CD pipeline decision record should include buf breaking --against .git#branch=main as a required gate for any service that uses Protobuf schemas, failing PRs that introduce wire-incompatible changes.
GraphQL schemas make nullability decisions in the schema that are costly to change after consumers have shipped. A field declared non-null (String!) in the schema is a promise to every consumer that the field will never be null; making it nullable later (String) is a breaking change for statically-typed consumers whose generated code does not handle the null case. The cost of a nullability mistake in GraphQL is therefore a schema change that requires coordinated consumer migration, the same structural problem as a required-field removal in REST. GraphQL's @deprecated directive is native to the schema, making field deprecation visible in the schema itself and in any introspection-based tooling the consumer uses — an improvement over REST's convention-based deprecation. Schema stitching and federation add complexity for multi-service GraphQL deployments: the stitched or federated schema is the contract that consumers depend on, and evolution of the individual service schemas must be coordinated to avoid breaking the composed schema. GraphQL is the correct choice when multiple consumers need different subsets of the same data at different freshness levels — the mobile app needs fewer fields than the data pipeline, the dashboard needs aggregations that the transactional API does not serve. The multi-tenancy decision record intersects with GraphQL schema design: tenant-scoped field visibility and isolation — which fields a tenant's query is permitted to request, which fields return tenant-filtered data — must be modeled in the schema and enforced by the resolver layer, and the schema design ADR must document the tenant isolation model for each field that has tenant-scoped access semantics.
AI chat session types and what each one misses
The API schema design decision follows a consistent pattern of AI chat sessions. The WhyChose extractor surfaces these sessions from AI chat history exports — the initial API design session, the breaking change session, and the migration planning session are reliably present, and the structural decisions they omit are consistent across the decision records reviewed. The schema shape is chosen in the first session, the extensibility contract is never documented, the first breaking change produces either a crash or a migration, and the migration planning session is the first time anyone calculates the consumer count.
The initial API response design session covers: what endpoints to create, what fields to include in the response, how to structure the data the consumer needs, what HTTP status codes to use for different outcomes. The session ends when the API returns the right data for the current consumer. What the session does not cover: whether the enum fields in the response are open or closed, whether the embedded related objects should be embedded or referenced, what the required-versus-optional field policy will be, what the breaking change taxonomy will be, and who will own the schema contract as the consumer count grows. These questions are not visible in the session because the product has one consumer and the schema shape that serves that consumer is the schema shape that is shipped. The decision made in this session — embed the billing address, annotate the status field with the current two enum values — is the schema shape every subsequent consumer will build against.
The "add a new status value to the subscription API" session covers: how to add the new enum value to the OpenAPI spec, how to update the serializer to emit the new value, how to write the migration for the new database enum value. The session ends when the new value is deployable. What the session misses: the question of whether adding a new enum value is a breaking change under the team's schema contract is not asked, because the schema contract was never documented. The engineer implementing the change makes a judgment — "adding a value to a string enum feels like a non-breaking addition" — and that judgment is coherent in isolation. The iOS consumer's judgment — "an OpenAPI enum annotation is exhaustive" — is also coherent in isolation. The open/closed enum contract documentation is the missing artifact that would have made both judgments visible in the same conversation, forcing a choice and recording the consequence. A schema design ADR with a documented open enum contract and a documented consumer obligation (handle unknown values with a default case) would have made the "add suspended" session a routine change review rather than a Tuesday morning incident.
The "we need to support multiple billing addresses" session covers: how to redesign the data model to support multiple addresses per account, how to write the database migration, how to update the API response structure. The session is focused on the new feature design and the migration plan. What the session misses: it is the first time the embedding decision from year one becomes a subject of discussion, but it is discussed as a migration constraint ("we have to maintain the old billingAddress field for backwards compatibility") rather than as a design decision whose rationale is being evaluated retrospectively. There is no moment in this session where the team asks "why did we embed the address object rather than use an ID reference in year one, and what would the migration cost difference have been?" — because the year-one decision was never documented, there is nothing to evaluate against. The new engineer who joins the team after the migration finds both billingAddress and billingAddresses in the response and has to ask a colleague why both exist — the dual-field window is not self-documenting, and the ADR that would explain the original embedding decision, the migration trigger, and the dual-field period's end date does not exist.
The "a consumer broke after our deployment" session covers: what error the consumer is reporting, what changed in the recent deployment, how to roll back the breaking change, how to restore the consumer's functionality. The session is reactive: the goal is to stop the incident, not to understand why the schema change was not identified as breaking before deployment. What the session misses: it is the clearest opportunity to drive the breaking change taxonomy and the consumer contract obligations into a documented ADR, but the postmortem pressure is on resolution rather than documentation. The iOS crash is resolved by rolling back the "suspended" value; the open/closed enum contract question is not asked because the incident is closed when the crash rate returns to baseline. Three months later, a different engineer needs to add another new subscription state, and the question of whether the enum is open or closed is still undocumented, and the iOS client is still running the exhaustive switch statement that was never updated. The authentication strategy decision record context applies here: auth token format decisions in the API response — whether the token is opaque, what fields a JWT contains, whether a session ID field is always present — affect which schema changes are breaking for consumers that parse the token structure, and those decisions belong in the same schema design ADR that documents the breaking change taxonomy for the rest of the response schema.
Five ADR sections for API schema design
An API schema design ADR that prevents consumer crashes, coordination-heavy migrations, and undocumented deprecations covers five sections that teams consistently omit from their OpenAPI specifications and API style guides.
First, response schema format, tooling, and CI enforcement. The ADR documents where the schema lives (co-located with the service under /openapi/, or in a shared schemas repository with a path per service and version), the schema format (OpenAPI 3.1 for REST, Protobuf 3 for gRPC, GraphQL SDL for GraphQL), the code generation tooling (oapi-codegen for Go, openapi-generator for multi-language, buf for Protobuf), and the CI enforcement gates. CI gates include: an openapi-diff step that compares the PR's schema against the main branch schema and fails the PR if the diff contains any change classified as breaking by the diff tool's rule set; a buf lint or buf breaking step for Protobuf schemas; and a consumer contract test suite using Pact or Spring Cloud Contract that verifies the producer schema satisfies the contracts registered by known consumers before deployment. The schema review checklist in the PR template asks: Is this change breaking per the ADR's breaking change taxonomy? If yes, have affected consumers been notified with the required notice period for their consumer tier? Is a dual-field window required? Has the schema owner approved the change? The schema owner role — a named engineer or team who approves any breaking change PR — is the governance mechanism that prevents breaking changes from shipping without explicit approval. Without a named owner, breaking change review is implicit in the general PR review process, which means it depends on the reviewer's individual knowledge of which consumers exist and what they depend on.
Second, breaking change taxonomy and compatibility rules. The ADR documents an explicit list of what constitutes a breaking change versus a non-breaking change in the team's schema. Non-breaking changes (may ship without a version bump or consumer notification): adding a new optional field to a response body, adding a new open-enum value to a field documented as open, adding a response header, adding a new endpoint, adding a new optional query parameter. These changes are non-breaking only when consumers satisfy the consumer contract obligations documented in section five — a consumer that validates the response with additionalProperties: false treats an added optional field as breaking, and that consumer is in violation of the consumer contract. Breaking changes (require consumer notification per the deprecation policy and potentially a version bump): removing any field from a response, changing the type of an existing field (string to integer, object to array, array to singular object), removing a value from a closed enum or removing a discriminated union variant, renaming a field without maintaining the old field as an alias for the duration of the dual-field window, making a required field optional (breaks consumers that depend on the field always being present), making an optional field required (breaks consumers that omit the field in requests), changing an embedded object field to an ID reference field. The breaking change authority is the schema owner: any PR that introduces a breaking change under this taxonomy requires the schema owner's approval and a migration plan before merge. The taxonomy makes the judgment explicit and consistent: the iOS crash happened because two engineers made different implicit judgments about whether adding a new enum value was breaking; with a taxonomy that classifies "adding a value to a closed enum" as breaking, the migration plan is required before the "suspended" value ships.
Third, deprecation policy. The ADR documents the minimum notice period before a field can be removed, the communication mechanism for deprecation notices, the dual-field window procedure, and the removal announcement process. Minimum notice periods, differentiated by consumer tier: external third-party integrations — 6 months from the deprecation announcement to the removal date, because enterprise customers have change management processes and integration update cycles that cannot be shortened by the API provider; mobile apps — 3 months, accounting for the development time to update the app, the app store review cycle (1-2 weeks for iOS), and the user adoption time for the new app version (customers who do not auto-update may still run the old version for weeks after the new version ships); internal services — 4 weeks, because internal services are under the API team's coordination and can be updated on a faster cycle than external consumers. Communication mechanism: a Deprecation response header (per RFC 8594) on all responses from the deprecated endpoint or field, with the deprecation date; a Sunset header (per RFC 8594) with the removal date; an entry in the API changelog at the deprecation announcement date with the field name, the replacement field or API pattern, the dual-field window start and end dates, and the migration guide; an email to registered API consumers for external third-party deprecations; an internal Slack announcement for internal service deprecations. Dual-field window procedure: during the dual-field window, the response emits both the deprecated field and its replacement; the deprecated field's value is derived from the replacement to ensure consistency, not separately maintained; the dual-field window duration matches the notice period for the longest-cycle consumer tier affected by the deprecation. Sunset monitoring: a CloudWatch metric or Datadog monitor tracks the request volume for the deprecated field path in the access logs (by checking for field access in API gateway logs); an alert fires when usage exceeds zero at T-minus-2-weeks before the removal date, identifying consumers that have not yet migrated and may need a deadline extension or a direct outreach. The three integrations that broke when billingAddress was removed at the end of the dual-field period are the consumers the sunset monitor would have identified and given a second notification before the removal date.
Fourth, enum and polymorphism contract. For each enum field in the API, the ADR documents whether it is open or closed in the field description, and the consumer obligation that follows from the contract. An open enum is documented in the field description as: "This field is an open enum. New values may be added in future API versions without a breaking change notice. Consumers MUST handle unknown values gracefully — typically by treating them as an unrecognized state and applying a default behavior — and MUST NOT throw an unhandled exception on receiving an undocumented value." A closed enum is documented as: "This field is a closed enum. The complete set of possible values is listed above. New values will not be added without a versioned API update and consumer migration notice. Consumers MAY write exhaustive handlers for the listed values." The subscription status field is an open enum — the product will add new states as the subscription lifecycle evolves. The account type field (individual, business, enterprise) is a closed enum — the account type taxonomy is stable and new types require a deliberate product decision. For discriminated union types — responses whose shape depends on a type discriminator field, such as a payment method response that may be { "type": "card", "last4": "..." } or { "type": "bank_account", "routing_number": "..." } — the contract documents how new union variants are added (announcement in the API changelog with the new type value and the new variant's schema), whether existing variants can be modified (non-breaking additions under the general compatibility rules; breaking changes require the standard deprecation process), and the consumer obligation for unknown variants (treat as an unrecognized payment method type and apply a fallback behavior; MUST NOT throw on an unknown type value). The discriminated union contract is the same open/closed model applied to the discriminator field's value set rather than a simple enum field.
Fifth, consumer contract obligations. The ADR documents the explicit requirements that every consumer of the API must satisfy to interoperate safely as the API evolves. These obligations are what allow the producer to make non-breaking additions safely, because the producer's ability to add optional fields and open enum values without triggering migrations depends on every consumer satisfying these obligations. First: consumers MUST handle unknown enum values in open enum fields without throwing an unhandled exception — the default case in a switch statement, the fallback branch in a conditional, the unknown-type rendering path in a UI component — because open enum fields may receive values not present in the current OpenAPI spec. Second: consumers MUST NOT assert additionalProperties: false on response schemas — the producer may add new optional fields to any response without a breaking change notice, and consumers that reject responses with unrecognized fields will break on non-breaking additions. Third: consumers MUST NOT depend on the ordering of fields within a JSON object — JSON objects are unordered by specification, and the API may reorder fields in the serializer output without notice. Fourth: consumers MUST NOT depend on the ordering of elements in array fields that represent unordered collections — the API may reorder array elements for performance or consistency reasons without notice; if ordering is significant, the API will document it explicitly in the field description. Fifth: consumers MUST handle null values for optional fields without throwing — an optional field that is absent from a response is equivalent to the field being present with a null value, and consumers must handle both cases. Sixth: consumers SHOULD implement the Deprecation header check to receive sunset date metadata — a consumer that reads the Deprecation and Sunset response headers and logs or alerts on their presence will discover that a field it uses has been deprecated before the removal date, rather than discovering it when the field disappears. These obligations belong in the ADR and in the API onboarding documentation that every new consumer team reads before building their integration. The 31% iOS crash rate is a violation of obligation one; the three integrations that broke at the end of the billingAddress dual-field period are integrations that were not receiving the Deprecation header alert because they had not implemented obligation six.
None of these five sections appear in the OpenAPI specification file or in the API style guide. They are the schema reasoning that every engineer who adds a new field, evaluates whether a change requires a version bump, plans a deprecation, or builds a new consumer depends on to understand what the API design system is designed to guarantee. The iOS crash, the 61-consumer migration, and the three integrations that broke at deprecation removal are not caused by poor engineering in the individual sessions. They are caused by a schema design that was chosen without being documented — without specifying the enum extensibility contract, the embedding rationale, the deprecation policy, or the consumer contract obligations — so that each engineer who later extended the API, built a new consumer, or planned a migration had no source of truth for what the schema was designed to guarantee and where its designed limits were. The WhyChose extractor surfaces the initial API design session, the breaking change session, and the migration planning session from AI chat history; the API schema design ADR is what takes the reasoning from those sessions and makes it legible to the team that inherits the API contract and must evolve it, deprecate it, or explain it to an enterprise customer whose integration broke on a Tuesday morning.
FAQs
What is the difference between open and closed enum contracts in an API response?
A closed enum contract means the producing service guarantees the field will only ever contain the currently-documented set of values. Adding a new value is a breaking change requiring a versioned API update and consumer migration notice. Consumers of a closed enum may write exhaustive switch statements or validation rules that reject unknown values, because the contract guarantees they will never receive one.
An open enum contract means the producing service may add new values at any time without a breaking change, and consumers MUST handle unknown values gracefully — a default case in a switch statement, a fallback rendering path — rather than throwing on an unrecognized value. An open enum is appropriate for fields that represent extensible classifications: subscription states that may grow as the product adds new states, event types that may expand as new features are added. OpenAPI 3.x has no first-class open/closed enum mechanism; the contract is documented in the field description and the API changelog policy.
The consequence of leaving the contract undocumented is that consumers make different assumptions. The iOS developer who wrote an exhaustive switch with no default case was following a reasonable convention. The backend developer who added "suspended" to a string enum was also following a reasonable convention. Both were coherent in isolation. Together they produced a 31% mobile crash rate and eleven days of data inconsistency while the App Store review processed the fix.
Why is embedding a related object directly in a response different from returning an ID reference?
Embedding creates structural coupling: the parent API response schema contracts every field of the embedded resource. If the embedded resource evolves — changes field types, adds required fields, needs to become a collection rather than a singular object — every consumer that reads any embedded field must be migrated simultaneously with the server change. An ID reference decouples the parent response from the referenced resource's evolution: the parent only contracts that billingAddressId is a string identifier; the shape of the resource at GET /billing-addresses/{id} can evolve independently with its own versioning strategy.
The performance tradeoff is real: embedding avoids an extra API call per response, while ID references require a second request to fetch the related resource. The cost that is not visible at design time is the migration cost when the embedded resource's shape changes. With 61 consumer integrations each reading account.billingAddress.street, changing the field to a collection requires an 8-month dual-field window and a coordinated migration across every integration. An ID reference at year one would have required 61 consumers to make an additional API call per account fetch, but would have allowed the address resource to evolve independently without a coordinated migration.
The embedding-versus-referencing decision should be made per relationship based on: stability of the embedded resource's shape (stable shapes favor embedding), access frequency of the related data (always accessed together favors embedding), and expected consumer count (low consumer count makes future migration cheaper and favors embedding).
What should an API schema design ADR document that teams typically skip?
Teams typically document the API style (REST, GraphQL, gRPC) and include an OpenAPI spec as the deliverable. The five sections that prevent consumer crashes, coordination-heavy migrations, and undocumented deprecations: first, the response schema format, tooling, and CI enforcement — where the schema lives, what generates code from it, and what CI gates (openapi-diff, buf breaking, Pact consumer contract tests) prevent breaking changes from shipping without approval; second, the breaking change taxonomy — an explicit list of breaking versus non-breaking changes in the team's schema, with a named schema owner who approves breaking change PRs; third, the deprecation policy — minimum notice periods per consumer tier (external: 6 months, mobile: 3 months, internal: 4 weeks), the Deprecation and Sunset header communication mechanism, the dual-field window procedure, and sunset usage monitoring that alerts before the removal date; fourth, the enum and polymorphism contract — for each enum field, whether it is open or closed documented in the field description, and the consumer obligation that follows; fifth, the consumer contract obligations — explicit requirements every consumer must satisfy: handle unknown open-enum values without throwing, do not assert additionalProperties: false on response schemas, do not depend on field or array ordering, handle null optional fields without throwing, implement the Deprecation header check.
None of these sections appear in the OpenAPI specification file. They are the schema reasoning that every engineer who adds a field, evaluates a version bump, plans a deprecation, or builds a new consumer depends on to understand what the API contract is designed to guarantee.