Skip to content

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:

  1. Close the gate harder — mark the enum @final and forbid additions via linter. Adding a value becomes a multi-PR ritual: first remove @final, then add the value.
  2. 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.status with values GRANTED / REJECTED. If a future business need adds PENDING or WITHDRAWN, 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 + @final gate 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 stable enum Currency { USD EUR GBP @final }.
  • @extensibleEnum on FIELD_DEFINITION — applied to a String! field to declare it open with a current allowlist. The not-yet-closed status: 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

Last updated · 501 distilled / 1,218 read