CONCEPT Cited by 1 source
Extensible enum¶
Definition¶
An extensible enum is a string-valued field that
carries an allowlist of currently-known values but is
explicitly declared at the type level to be open to
future additions without being a breaking change. The field's
transport type is a string (String in GraphQL, string in
OpenAPI); the allowlist lives in a sidecar annotation (a
directive, x-extensible-enum extension, etc.) rather than
as a closed GraphQL/JSON-Schema enum.
The convention originates from Zalando's RESTful API
Guidelines,
where it is spelled x-extensible-enum — an OpenAPI
vendor extension that names the allowlist alongside a
type: string. The same idiom ports to GraphQL via a
custom directive:
directive @extensibleEnum(values: [String!]!) on FIELD_DEFINITION
type CustomerConsent {
status: String! @extensibleEnum(values: ["GRANTED", "REJECTED"])
}
The field type is String!, not an enum. Clients see a
string they should treat as "one of these known values,
possibly something else I should ignore gracefully"
(Source: sources/2023-10-18-zalando-understanding-graphql-directives-practical-use-cases-at-zalando).
Why not use a native enum?¶
Native GraphQL / OpenAPI enums have a well-known evolution hazard: adding a value is not a breaking change by the compatibility rules, but it is a "dangerous change" — existing client binaries in the wild (especially mobile apps shipped to app stores) may lack a handler for the new value. Unhandled-enum crashes are a class of production bug that no amount of type-system tightening prevents at the native-enum level.
Two orthogonal responses to the hazard exist:
- Close the gate harder — mark the enum
@finaland forbid additions via linter. Adding a value becomes a multi-PR ritual: first remove@final, then add the value. - Open the gate wider — declare the field extensible and force clients to defensively handle unknown values from day one. The field never becomes unsafe-to-extend; its openness is a property of its type.
Extensible enum is the open-the-gate-wider choice. @final
is the close-the-gate-harder choice. The two are not
opposites — they are different tools for different
semantics.
When extensible enum is the right choice¶
- One-off use-case fields. The post's motivating
example is
CustomerConsent.statuswith valuesGRANTED/REJECTED. If a future business need addsPENDINGorWITHDRAWN, the field should extend; it is not a stable-closed-set concept. - Fields that sit on top of external systems. Status codes from payment processors, shipping carriers, or inventory systems tend to grow over time; the GraphQL field is a pass-through and has no business being closed.
- Fields where client-side unknown-value handling is safe. If the UI can degrade gracefully on an unknown value (display the raw string, skip the icon lookup, hide the badge), the extensibility is cheap.
When extensible enum is the wrong choice:
- Business-logic-critical dispatch. If the server
branches on the enum value in privileged ways (e.g.
PermissionLevel), a closed enum +@finalgate is safer. - Fields whose unknown-value handling would surface wrong data. If an unknown value leads the client to the wrong UX, extensibility is harmful.
The adoption-ergonomics payoff¶
Zalando's post names a specific observation about contributor behaviour: "we found that contributors to the schema are more likely to think about the evolution of schema. We also noticed that contributors are more likely to use this directive for defining enums than the GraphQL native enum, as this directive is more explicit about the extensibility of the enum."
The directive-shaped declaration makes the extensibility
policy visible at the field definition site. A native
enum Status { GRANTED REJECTED } does not tell the
reader "this set may grow"; a String! @extensibleEnum
does. Over time, this nudges schema authors toward the
extensible default.
Relationship to the final-enum counterpart¶
Zalando's schema uses both directives at different points:
@final on ENUM_VALUE— applied to values of a closed enum to prevent new values from being added (linter-enforced). The ideal stableenum Currency { USD EUR GBP @final }.@extensibleEnum on FIELD_DEFINITION— applied to aString!field to declare it open with a current allowlist. The not-yet-closedstatus: String! @extensibleEnum(...).
Fields start extensible; when the value set is known to be
closed and stable, they can be migrated to a native enum
with @final marks. The two directives track different
stages of a field's lifecycle.
Seen in¶
- Zalando RESTful API Guidelines —
x-extensible-enumas the OpenAPI-side convention, §112. - Zalando UBFF —
@extensibleEnumas the GraphQL-side port of the same convention; canonical example isCustomerConsent.statuswithvalues: ["GRANTED", "REJECTED"](sources/2023-10-18-zalando-understanding-graphql-directives-practical-use-cases-at-zalando, systems/zalando-graphql-ubff).
Related¶
- concepts/final-enum — the close-the-gate-harder counterpart directive.
- concepts/backward-compatibility · concepts/schema-evolution — the broader API-evolution discipline this sits inside.
- concepts/api-first-principle — Zalando's overarching API design ethos that motivates both the OpenAPI and GraphQL extensible-enum conventions.
- concepts/graphql-schema-directive — the primitive carrying the annotation.
- systems/graphql · systems/zalando-graphql-ubff