skip to content

Enums & Input Objects

Enum values as a closed set of unquoted names, and input objects as the argument-side counterpart to objects. Interviewers use the split to check you know why an object type cannot be an argument.

part ofGraphQLoverview, primer and where to startread it →
on this pageshow

questions

4

In GraphQL, why is an enum value unquoted in a query document but a quoted string in the JSON response?

level: juniorimportance: must knowfreq 55%

answer

  1. One value, two notations
  2. The document is not JSON
  3. JSON has no enum kind
  4. Bare name in, quoted name out
  5. Names travel, numbers never do

basics

~20 s

GraphQL's own grammar has an enum-value token, so a value like STORED is written as a bare name in a document. JSON has no enum token, so the same value crosses the wire as a string holding that name.

solid answer

~50 s

An enum type declares a closed set of named values. Inside SDL and inside an executable document those values are written as bare names - `parcelsAtLocker(state: STORED)` - because the GraphQL language has a distinct enum-value token; the names `true`, `false` and `null` are excluded because the grammar already spells those as other tokens. JSON has only objects, arrays, strings, numbers, booleans and null, so wherever the value crosses into JSON it is carried as a **string of the value's name**: `"state": "STORED"` in a request's variables map, and `"state": "STORED"` again in the response `data`. That asymmetry has a practical edge: writing `state: "STORED"` as a literal in the document is a String, not an enum value, and the request is rejected, while the same quoted form in the variables map is exactly right. On the way out the server maps whatever it stores internally onto the declared value name.

code

graphql · 22 lines
graphql
enum ParcelState {
  AWAITING_DROP
  STORED
  COLLECTED
  EXPIRED
}

type Parcel {
  id: ID!
  state: ParcelState!
}

type Query {
  parcelsAtLocker(lockerId: ID!, state: ParcelState!): [Parcel!]!
}

query StoredAtLocker4217 {
  parcelsAtLocker(lockerId: "4217", state: STORED) {
    id
    state
  }
}

go deeper

for a junior

Be ready to write an enum in SDL and use one of its values in a query without quotes, and to point at the quoted form in the variables map and in the response as the same value in JSON notation.

for a middle

An interviewer expects you to name the two coercions: the input side matching a literal name or a JSON string against the declared values, and the result side mapping an internal representation back onto a value name.

for a senior

Show the operational consequence: an unmapped internal value fails result coercion rather than leaking, so a closed enum is a promise you have to keep as upstream systems change their own status vocabularies.

for a principal

Own the modelling call - which closed sets deserve an enum at all, given that names are the whole contract and every consumer's generated code hard-codes the set you publish.

## An enum type is a closed set of names An enum type definition lists the values a field or argument of that type may take, and nothing else is legal: ```graphql enum ParcelState { AWAITING_DROP STORED COLLECTED EXPIRED } ``` In a parcel-locker graph this says a `Parcel.state` is one of four names. The values are **names**, not strings: they follow the same name grammar as a field or type name (a leading letter or underscore, then letters, digits or underscores), and the three names `true`, `false` and `null` are excluded because the document grammar already reads those as the boolean and null tokens. SCREAMING CASE is a very widespread convention, not a rule - `Stored` would be a legal value name and a bad one. ## Two languages, two spellings of the same value The confusion this question probes comes from the fact that a GraphQL request travels in two languages at once. The **document** is written in the GraphQL language, which has an enum-value token, so the value is a bare word. The **variables map** and the **response body** are usually JSON, and JSON has exactly six kinds of value - object, array, string, number, boolean, null - none of which is an enum. So the transport spells the value with the closest thing it has: a string that carries the value's name. ## Input coercion: literal versus variable Both of these are correct, and they look different: ```graphql { parcelsAtLocker(lockerId: "4217", state: STORED) { id state } } ``` ```json { "query": "query($state: ParcelState!) { parcelsAtLocker(lockerId: \"4217\", state: $state) { id state } }", "variables": { "state": "STORED" } } ``` In the first, `STORED` is an enum-value literal inside the document. In the second, the document names a variable and the value arrives in JSON, so it is the string `"STORED"`, which the server coerces by matching it against the enum's value names. Two mistakes follow directly. Writing `state: "STORED"` **inside the document** supplies a String where a `ParcelState` is expected, and the request never runs. And declaring the variable as `query($state: String!)` and then using `$state` for a `ParcelState!` argument is rejected too: a variable's declared type has to be usable at the position it is used, and String is not ParcelState, however identical the JSON looks. ## Result coercion: back out as a name On the way out the direction reverses. The server holds the state in whatever form it likes - a single-character column, a small integer, a language-level enum - and result coercion maps that internal value onto one of the declared value names, then serializes the name as a JSON string: ```json { "data": { "parcelsAtLocker": [ { "id": "p-38", "state": "STORED" } ] } } ``` If the internal value maps to nothing in the set - a downstream service in an 11-service graph returns a status nobody added to the schema - coercion cannot invent a name. That field raises a field error instead of returning a made-up value, which is the whole point of a closed set: the schema's promise is that a client will only ever see one of the four names. ## Why the names, and only the names, are the contract Nothing positional ever reaches the wire. There is no ordinal, no index, no numeric mapping - so reordering the values in SDL is invisible to every client, while renaming one is a change every client sees. Internal numbers are an implementation detail of one server, and two servers behind the same schema may number them differently without anyone noticing. Because the set is closed and introspectable, tooling can read it: a typed client generator turns `ParcelState` into a four-member union or a language enum and makes an unhandled case a compile error. That is the practical difference between an enum field and a `String` field carrying the same letters. A String promises nothing; the enum promises the set. Individual enum values can also carry a description and can be marked deprecated with the built-in `@deprecated` directive, and a deprecated value is hidden from introspection listings unless the caller explicitly asks to include deprecated entries. ## What to keep hold of Bare name in the document, quoted name in JSON, and a name - never a number - is what the schema promises. Everything a candidate gets wrong here is a variation on forgetting that the document and the variables map are two different languages describing the same value.

  • Can an enum value be named `true`, `false` or `null`?
    No. The document grammar already reads those three words as the boolean and null tokens, so they are excluded from enum value names. Every other name that matches the ordinary name grammar is legal, including lowercase names and names beginning with an underscore, though uppercase is the near-universal convention.
  • What happens if a resolver returns a value the enum does not contain?
    Result coercion fails for that field. The server cannot serialize a name that is not in the declared set, so the field raises a field error rather than emitting the unknown value. That is the guarantee an enum buys a client: only the declared names ever arrive.
  • Does reordering the values in an enum definition change the wire contract?
    No. Only names cross the wire - there is no ordinal or index - so reordering is invisible to clients and to stored documents. Renaming a value is the change that matters, because a name is the entire contract for that value.

Think of a boarding-pass gate letter. On the printed pass it appears as a plain character; read out over the intercom it becomes a spoken word. It is one value in two notations, and neither one is a different gate.

saying these in an interview costs you the question

  • Says enum values are quoted strings inside the query document
  • Thinks the response carries the enum's numeric ordinal
  • Believes a String-typed variable can feed an enum argument
  • Assumes SCREAMING_CASE naming is enforced by the specification
  • Treats an enum field as a String with documentation attached
  • Thinks reordering enum values is a breaking wire change

context

open as a page

Why does GraphQL split types into input and output kinds, and which kinds sit on both sides?

level: middleimportance: must knowfreq 62%

basics

~20 s

A caller supplies complete values while a server returns things a client selects fields from, so the two directions need different type kinds. Scalars and enums work in both directions; object, interface and union types are output-only, and input object types are input-only.

open as a page

In a large GraphQL schema, how do you manage the input object types that shadow output object types?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Treat each input as the argument shape of one operation rather than a writable copy of an entity. Divergence from the output type is expected and should be deliberate; the failure mode is a shared entity-shaped input reused by every mutation.

open as a page

What does the @oneOf directive on a GraphQL input object type require, and is it in the specification?

level: middleimportance: nice to knowfreq 18%

basics

~20 s

It marks an input object type whose caller must supply exactly one of its fields, with a non-null value. Every field of such an input must be nullable and carry no default. It is a working-draft addition, not part of the last ratified specification edition.

open as a page