How does a GraphQL server coerce the values in a request's variables map to their declared types?
answer
- Values are checked before anything executes
- Scalars are strict; ID is the exception
- Enums arrive as exact-match strings
- A single value becomes a one-item list
- A failure is a request error
basics
~20 sBefore execution the server coerces each supplied value to its declared type: scalars by strict per-type rules, enums from a string matching a member name exactly, lists item by item. A value that will not coerce is a request error.
solid answer
~50 sCoercion happens once per request, after the document parses and validates and **before any field executes**. For each variable definition the server takes the value under that name, checks it against the declared type and produces a coerced value. The built-in scalars are strict: `Int` accepts only an integer inside the signed 32-bit range, `Float` accepts an integer or a float, `String` accepts only a string - `"25"` for an `Int` is rejected, not parsed - `Boolean` only a boolean, and `ID` accepts a string or an integer. An **enum** value arrives in a values map as a string that must match a declared member name exactly, case-sensitively, because the unquoted enum literal form exists only inside the document. For a **list** type, each item is coerced by the item type, and a value that is not a list is wrapped into a one-item list. Anything that fails is a request error, so nothing executes.
code
graphql · 11 linesquery CatalogueSearch(
$first: Int!
$markets: [Market!]
$minDurationSeconds: Float
$labelId: ID
) {
tracks(first: $first, markets: $markets, minDurationSeconds: $minDurationSeconds, labelId: $labelId) {
id
title
}
}go deeper
Recall that supplied values are checked against the declared types before anything runs, and that the check is strict: a quoted number is not an Int. Knowing enums travel as exact-match strings will save you a real debugging session.
Walk the algorithm: absent versus defaulted versus null, then per-type coercion. Name the specific rules an interviewer probes - the 32-bit Int range, ID accepting a string or an integer, and a single value being promoted to a one-item list.
Use the ordering as a diagnostic. A response where nothing executed points at parse, validation or coercion, never at a backend, and that lets you triage a client report without reading a single resolver. Be ready to say why lenient coercion would be worse than strict.
Own where input strictness belongs. Coercion gives you type-level guarantees for free at the edge; anything past that - ranges, cross-field consistency, tenancy - is your validation layer's job, and deciding which constraints move into the type system is a schema-design tradeoff you should be able to argue.
## Where coercion sits A GraphQL request goes through fixed phases: the document is **parsed**, then **validated** against the schema, then its variable values are **coerced**, and only then does execution begin. Coercion is the first phase that looks at the values map at all - validation is purely static and needs no values. That ordering has a practical consequence worth stating up front: a coercion failure means *nothing ran*. No resolver was called, no backend was touched, no partial data exists. ## The per-variable algorithm For every variable definition in the operation, in order, the server does roughly this: 1. Look up the variable's name in the supplied values map. 2. If the name is **absent** and the definition has a **default**, use the default. 3. If it is absent, has no default, and the declared type is **non-null**, that is a request error. 4. If it is absent, has no default and the type is nullable, the variable is simply *not provided* - it is left out of the coerced set entirely. 5. If the value **is** present and is `null`, that is a request error when the declared type is non-null, and otherwise coerces to null. 6. Otherwise, coerce the value by the declared type's input coercion rules; failure is a request error. ## Scalar rules, and how strict they are The built-in scalars are deliberately unforgiving, because silent string-to-number conversion is a bug factory: - **`Int`** accepts only an integer value, and only within the signed 32-bit range. A fractional number is rejected, and so is a numeric *string* such as `"25"`. A track-count over two billion needs a different scalar, not a bigger `Int`. - **`Float`** accepts an integer or a floating-point value - an integer widens - but not a string, and not a non-finite value. - **`String`** accepts only a string. A number or a boolean is rejected rather than stringified. - **`Boolean`** accepts only a boolean. `"true"` and `1` are not booleans. - **`ID`** is the deliberate exception: it accepts a string **or** an integer, and an integer is coerced to its string form. This exists because identifiers turn up as both in real payloads. A **custom scalar** coerces however its implementation says - the type system declares only its name, so its accepted input shape is a property of the server, not of the specification. ## Enums in a values map Inside a document an enum value is an unquoted name: `markets: [GB, IE]`. Inside a values map there is no such literal - JSON has no bare-word type - so an enum value is sent as a **string** whose contents must exactly match a declared member name. `"GB"` coerces to the `GB` member; `"gb"` does not, because member names are case-sensitive; `"UNITED_KINGDOM"` does not, because it is not a member. This is the single most common surprise in the whole coercion story, because the same value is written two different ways depending on where it appears. ## List input coercion List coercion has one rule that catches people out. If the declared type is a list and the supplied value **is** a list, every item is coerced by the item type and any single item failing fails the whole request. If the supplied value is **not** a list and is not null, it is coerced by the item type and the result is wrapped in a list of one. So for `$markets: [Market!]`, the value `"GB"` yields `["GB"]` coerced to `[GB]`, exactly as if `["GB"]` had been sent. The wrapping applies at each level of a nested list type, so `1` supplied for `[[Int]]` yields `[[1]]`. Note the asymmetry: a single value is promoted to a list, but a list is never demoted to a single value. Sending `["GB"]` for a plain `Market` variable is an error. ## Input objects An input object value must be a map. Each field is coerced by that field's type; a field left out takes that field's default if it has one; supplying a name that is not a declared field is an error rather than being ignored; and supplying null for a non-null field is an error. ## What happens when it fails Every failure above is a **request error**, not a field error. Execution never starts, so there is no partial data to report - unlike a resolver that throws mid-execution, which leaves the rest of the response intact. A useful diagnostic habit follows from that distinction: if a response came back with nothing executed, look at the request's variables and the document, not at a backend. One more implication for testing. Because coercion is driven entirely by the declared types, the same values map can be valid for one operation and invalid for another that declares the same names with tighter types. Coercion answers "is this value acceptable **here**", never "is this value well-formed in general".
- Why is an enum written unquoted in the document but quoted in the values map?Because they are two different notations. The document has its own grammar, which includes an enum literal: a bare name such as `GB`, distinct from the string `"GB"`. A values map is usually JSON, which has no bare-word type at all, so the only way to carry an enum member is a string. The server knows the declared type is an enum and matches that string against the member names exactly, case-sensitively.
- Does a value that fails coercion produce a partial response with the rest of the data?No. Coercion runs before execution begins, so a failure means no field resolver ever ran and there is no partial data to return - it is a request error, not a field error. That is a useful diagnostic split: an empty result with nothing executed points at the document or the variables, whereas holes inside a populated response point at a resolver.
- If `"GB"` is accepted for a `[Market!]` variable, is `["GB"]` accepted for a plain `Market` variable?No, and the asymmetry is deliberate. Coercion promotes a single value into a one-item list because a client that has one value should not have to know the field takes many. It never goes the other way: a list supplied where a single value is declared has no unambiguous single value to reduce to, so it is a request error.
saying these in an interview costs you the question
- Thinks a numeric string coerces to Int
- Sends an enum member as an unquoted name in JSON
- Assumes enum member matching is case-insensitive
- Believes a list variable always needs an array
- Expects a coercion failure to still return partial data
- Assumes Int is arbitrary precision rather than 32-bit