skip to content

In a GraphQL partial update, how does the server tell an omitted input field from an explicit null?

level: middleimportance: must knowfreq 66%

answer

  1. Two intents, one JSON key
  2. Coercion produces a map, not a struct
  3. Key absent versus key present and null
  4. A default on the field destroys the distinction
  5. Wrapper input or explicit clear list

basics

~20 s

GraphQL input coercion keeps them apart: an omitted field with no default is absent from the coerced input, while an explicit null is present with the value null. Whether your server surfaces that difference is a mapping question.

solid answer

~50 s

Coercion produces a map. A field the caller did not send, and that declares no default, does not appear in that map at all; a field sent as `null` appears with the value null. So the tri-state — absent, null, value — exists at the protocol level, and a partial update can genuinely mean "leave `dietaryNote` alone" versus "clear `dietaryNote`". The trap is the language mapping: bind that map eagerly to an object whose `dietaryNote` member is a nullable string and both cases collapse to null. Most servers offer a way out — reading the input as a map, or a wrapper that is itself optional. If you cannot rely on that, put the intent in the schema: a nullable wrapper input where absence means no change and `{ value: null }` means clear, or an explicit list of fields to clear. Never give such a field a default; that destroys absence.

code

graphql · 6 lines
graphql
input UpdateReservationInput {
  reservationId: ID!
  partySize: Int
  dietaryNote: String
  tableNumber: Int
}

go deeper

for a junior

Know that leaving a field out of an update input and sending it as null are two different requests, and that null normally means "clear this value" while omission means "no change".

for a middle

Explain where the distinction actually lives — coercion produces a map in which an omitted, undefaulted field is simply absent — and how a naive binding to a nullable member throws it away before your code runs.

for a senior

Show judgement about which shape you would publish: rely on absence, add a nullable wrapper input, or model an explicit clear list. Say what each costs callers, and how you would prove clearing works.

for a principal

Own the consistency decision. One update-semantics convention written down and enforced in review beats a per-mutation choice that leaves a 4-person platform team re-deriving the rule for every new input type.

## Two different requests that look the same A restaurant ordering graph publishes a partial update: ```graphql input UpdateReservationInput { reservationId: ID! partySize: Int dietaryNote: String tableNumber: Int } ``` A caller has two distinct intents for `dietaryNote` and only one field to express them with. "The diner no longer has a dietary note — clear it" and "I am only changing the party size, leave the note alone" must not do the same thing. This is the single most common design problem in GraphQL input objects, and interviewers ask it because the wrong answer silently destroys data. ## What the specification actually gives you The distinction survives at the protocol level. Input coercion produces a **map**, and the rule for an omitted field with no default is that the field is *absent from that map* — not present with the value null. A field sent as `null` on a nullable type is present, with the value null. So there are three states, and GraphQL keeps them apart: * **absent** — the caller said nothing about this field * **null** — the caller explicitly asked for no value * **a value** — the caller asked for that value The same absent-rather-than-null treatment applies when the field's value is a variable that the request did not supply: the field is treated as not provided rather than as provided-null. Two things destroy the distinction before your code ever sees it. Declaring a **default** on the field fills the hole in during coercion, so the key is always present. Declaring the field **Non-Null** removes null from the alphabet, so clearing becomes unexpressible. ## Where it gets lost anyway Servers lose the tri-state in the **language mapping**, not in the protocol. If the framework deserializes the coerced input into a plain object with a nullable `dietaryNote` member, absent and null both land as null and the information is gone. That is why the honest answer to this question has two halves: what GraphQL guarantees, and how your particular server surfaces it. The usual escapes, in rough order of preference: * Read the coerced input as a **map** and ask whether the key is present before touching the column. * Use whatever "was this provided?" wrapper the server offers — an optional-of-optional, so the outer level answers *provided?* and the inner one answers *null?*. * If neither is available, stop relying on absence and **put the intent in the schema**. ```pseudocode update_reservation(coerced): patch = {} if "partySize" in coerced: patch.party_size = coerced["partySize"] if "dietaryNote" in coerced: patch.dietary_note = coerced["dietaryNote"] # may be null apply(patch) ``` ## Putting the intent in the schema instead Three shapes are common, and each moves the tri-state out of the coercion map and into types the caller can see: **A wrapper input.** `dietaryNote: NullableStringUpdate` where the wrapper itself is nullable and holds one field: omit the wrapper for "no change", send `{ value: null }` for "clear", send `{ value: "shellfish allergy" }` to set. Explicit, self-documenting, and immune to how your server binds inputs — at the cost of a noisier document. **An explicit clear list.** `clear: [ReservationField!]` alongside the normal fields, where naming a field in the list means "set it to null". Enumerable, easy to authorize field by field, and awkward once two fields need clearing in different ways. **Separate mutations.** `clearReservationDietaryNote(reservationId: ID!)` beside the general update. Unambiguous, and it grows a mutation per clearable field, which is only worth it when clearing has different rules or different permissions from setting. ## Evolution, and why the choice is sticky Whichever shape you pick, switching later is a **breaking change for senders**. Suppose the team started with a `clearDietaryNote: Boolean` flag and now prefers explicit null. Removing the flag means every document that still sends it fails validation before execution, and the caller that still sends it is often a shipped mobile build that cannot be redeployed the way the server can. The safe order is: add the new shape, migrate senders, watch field usage until the old shape is unused, then remove. ## How to prove it works Test the three states as three separate cases, because two of them look identical in a careless test: update with the field omitted and assert the stored note is unchanged; update with the field sent as null and assert it is cleared; update with a value and assert it is set. A test suite that only ever sends values will pass happily on a server that blanks the note on every update.

  • Your server binds the input to an object whose nullable members default to null. How do you recover the distinction?
    Stop binding the whole input eagerly. Most servers expose the coerced input as a map, or offer a wrapper that says whether the key was present; read absence from that and map to your own type afterwards. If the framework genuinely cannot tell you, move the distinction into the schema — a nullable wrapper input, or a `clear: [ReservationField!]` list — so the intent survives regardless of how binding works.
  • Why is a default value on a partial-update input field a mistake?
    Because coercion fills the omitted field in, so the key is always present and "do not touch this" becomes unexpressible. `dietaryNote: String = ""` means every update that never mentions the note silently blanks it. Creation inputs are where defaults belong; update inputs need absence to keep meaning something.
  • Once the mutation has shipped, can the team swap a `clearDietaryNote: Boolean` flag for the absent-versus-null design?
    Not in one step. Removing the flag makes every document that still sends it fail validation before execution, and the caller still sending it is often a shipped mobile build that cannot be redeployed the way a server can. Add the new shape, migrate senders, watch field usage until the flag is unused, then remove it.
  • How would you test that the three states behave differently?
    As three separate cases, because two of them look alike in a careless test. Update with the field omitted and assert the stored value is unchanged; update with the field sent as null and assert it is cleared; update with a value and assert it is set. A suite that only ever sends values passes happily on a server that blanks the column on every update.

A blank box on a form and a box that was never printed are different answers. The blank box says "no dietary note"; the missing box says "I am not talking about the note today".

saying these in an interview costs you the question

  • Claims GraphQL cannot distinguish an absent field from null
  • Treats a missing key and a null value as the same intent
  • Adds a default to an update field to be safe
  • Makes every update field Non-Null so nothing can be cleared
  • Assumes a client library always sends keys it holds as undefined
  • Says a null input field means a resolver failed

context