skip to content

Why can renaming a GraphQL enum value break responses at execution rather than at validation?

level: seniorimportance: nice to knowfreq 21%

answer

  1. Two directions, two mechanisms
  2. Only one of them runs early
  3. The rows never re-validate
  4. It fails per object, not per request
  5. Result coercion lives inside completion

basics

~20 s

Validation only checks enum values sent in. Values sent out are checked during value completion, when the leaf is serialized: a stored value that maps to no declared enum value raises a field error, per object, long after every static check passed.

solid answer

~50 s

An enum is checked in two directions by two different mechanisms. Inbound literals and variables are checked before execution - an undeclared value makes the document invalid or the request fail at coercion. Outbound values are checked during **value completion**: when a leaf field typed as an enum is completed, result coercion must produce one of the declared values, and raises a field error when it cannot. Rename `PAPER` to `WORKS_ON_PAPER` in a museum collection graph and the SDL is valid, every document re-validates, and the build is green - but rows still holding the old string now fail coercion one artwork at a time. The failure is data-dependent, so a smoke test over recently written rows passes; it is invisible to validation and to schema linting, because neither can see what is in storage.

code

graphql · 11 lines
graphql
enum MediumCategory {
  PAINTING
  SCULPTURE
  WORKS_ON_PAPER   # was PAPER until this release
  TEXTILE
}

type Artwork {
  accessionNumber: String!
  medium: MediumCategory!
}

go deeper

for a junior

Know that an enum's declared values constrain what a field may return, not just what a client may send, and that the check on the way out happens while the response is being built rather than up front.

for a middle

Explain the two directions precisely: input coercion checked before execution, result coercion inside value completion. Be able to say why a green build and a valid document tell you nothing about whether stored values still map.

for a senior

Show the diagnosis. Recognise a per-object, data-dependent error rate, trace it to leaf serialization, and name the guard that would have caught it - a totality check over stored values and a data migration shipped with the schema change.

for a principal

Own enum policy across the estate: whether stored representations are decoupled from the declared names, who is allowed to rename a value, and how any leaf type whose mapping from storage is not total gets a deliberate unmapped branch instead of an execution-time surprise.

## Enums are checked in two directions, by two different mechanisms A GraphQL enum type declares a fixed set of values, and those values travel in both directions: inward as arguments and variables, outward as the results of fields typed with that enum. The two directions are checked by completely different machinery, at different times, and only one of them happens before execution. **Inward** is input coercion. A literal enum value written in a document is checked by validation: if `medium: PAPER` names a value the enum does not declare, the document is invalid and the request fails before a single resolver runs. A variable's value is checked at coercion time when the request is prepared, and produces a request error. Either way the client is told, definitively, before any work happens. **Outward** is result coercion, and it is part of value completion. When the executor completes a leaf field whose declared type is an enum, it hands the resolved value to the result-coercion routine the type system supplies. That routine must produce one of the declared values; the specification requires servers to return one of the defined set, and to raise a field error when a reasonable coercion is not possible. Nothing about that check can happen earlier, because the value did not exist earlier - it came out of a resolver, out of a row, out of an upstream call. ## The rename, and why it hides Take a museum collection graph whose `Artwork.medium` is typed `MediumCategory!`, and a schema change that renames the value `PAPER` to `WORKS_ON_PAPER` because curators asked for it. The SDL is valid. Every document in the repository is re-validated against it and the ones using the old literal are fixed. The build is green, and the change ships alongside a mobile release. The rows are the problem. The server maps a stored string to an enum value by name, and the collection database still holds `PAPER` on tens of thousands of older accessions. Completion now reaches those rows, asks the enum to coerce `PAPER`, gets nothing legal back, and raises a field error at each one. Because `medium` is declared `MediumCategory!`, the error cannot be absorbed at that position, and the artwork objects carrying old rows come back as holes with errors beside them. Three properties of this failure are what make it an interview question rather than a bug report: - **It is data-dependent.** Newer accessions written after the migration script are fine. A smoke test that queries the three most recently catalogued works passes cleanly. - **It is invisible to validation and to schema linting.** Both look at the schema and the documents. Neither can see what strings are in the database. - **It is invisible to a compile.** A typed client regenerated against the new schema compiles against the new value; the previously shipped mobile build carries the old one. Neither build is told that the *rows* disagree with both. ## Where the check actually belongs Because the failure lives between the storage representation and the schema, the guard has to live there too. The pragmatic answers a senior candidate offers: - A **totality check over the data**, run as part of the change: select the distinct stored values for the column and assert every one maps to a declared enum value. On a finite domain this is cheap and it is the only check that actually sees the failure mode. - **Migrate the data in the same change as the schema**, and treat an enum rename as a data change that happens to touch SDL, not a schema change that happens to touch data. - **An explicit unmapped branch** in whichever code maps stored values to enum values, so that an unknown string produces a deliberate outcome - a chosen fallback value on a nullable field, or a clear error message naming the offending string - instead of a generic coercion failure. - **A contract test that executes the query over realistic fixtures**, including historical rows. Validation-level tests will never catch this; only running the field over old data will. ## The general rule to state out loud The rule generalises past enums, and saying so is what separates a good answer from a lucky one: **anything a leaf field must serialize can fail at execution time, on some values and not others.** Result coercion sits inside value completion, at the very bottom of the response tree, after every static check has already passed. Whenever the mapping between a server's internal representation and a declared leaf type is not total, the schema is making a promise the data can break, and the break shows up per object, in production, at whatever rate the offending rows appear.

  • Where would you catch this before it reaches production?
    In the data, because that is where the failure lives. Select the distinct stored values for the column and assert each maps to a declared enum value, run that check as part of the change, and migrate the rows in the same change as the SDL. A contract test that executes the field over historical fixtures also catches it; a validation-level test never will.
  • Does the same execution-time failure apply to scalar fields?
    Yes. Result coercion is part of completing any leaf, so any value a leaf field cannot serialize into a legal value of its declared type raises a field error at that position. Enums make it vivid because the legal set is small and visible, but the general rule is that leaf serialization can fail on some values and not others.
  • Why does the failure show up on some objects and not others?
    Because coercion runs per completed value. Rows written after the migration coerce cleanly and rows written before it do not, so the error rate simply tracks how often historical rows appear in a response. That is what makes a smoke test over the newest records so misleading.

saying these in an interview costs you the question

  • Says validation would have caught the mismatch
  • Thinks an enum only constrains inputs
  • Expects the raw stored string to pass through
  • Blames the client's response parser
  • Assumes an unmappable value quietly becomes null
  • Treats an enum rename as a schema-only change

context