skip to content

In GraphQL, why is relaxing an argument to nullable safe when relaxing an output field is not?

level: seniorimportance: should knowfreq 46%

answer

  1. Which side already shipped the code?
  2. Client writes arguments, reads fields
  3. Loosen the shipped side's obligation
  4. Non-Null with a default is still optional
  5. Absence acquires a meaning downstream

basics

~20 s

Because the client writes arguments and reads output fields. Relaxing an argument only widens what the client is allowed to send, so every existing document is still valid; relaxing an output field widens what the client must be prepared to receive.

solid answer

~40 s

Nullability changes are safe in whichever direction *loosens the obligation on the party that has already shipped code*. For outputs the client reads, so the schema may narrow what it receives — nullable to Non-Null — and never widen it. For arguments and input fields the client writes, so the schema may widen what it accepts — Non-Null to nullable — and never narrow it. Tightening `sectionId: ID` to `sectionId: ID!` invalidates every document that omitted it. One wrinkle blunts the symmetry: an argument is *required* only when its type is Non-Null **and** it has no default value, so `limit: Int! = 25` is Non-Null yet perfectly optional to supply. And type-safe is not the same as semantically safe — relaxing an argument changes what a missing value means on the server.

code

graphql · 3 lines
graphql
type Query {
  enrollments(sectionId: ID!, limit: Int! = 25): [Enrollment!]!
}

go deeper

for a junior

Know that arguments and output fields do not behave the same way. Remember the concrete pair: adding ! to an output field is safe for clients, adding ! to an argument rejects every document that left it out.

for a middle

Explain the reversal from the direction of flow rather than as two memorised facts, and be ready to say what actually fails on a tightened argument — the operation is rejected during validation and never executes.

for a senior

Volunteer the default-value rule that makes a Non-Null argument optional, and show you know a validating change can still be unsafe: name something downstream, such as a response cache key or an authorization check, that a newly absent argument silently changes.

for a principal

Own the framing that input loosening is where semantic risk hides precisely because the type checker approves it. Be ready to describe what review or check you would put in front of an argument becoming optional on a viewer-scoped field.

## The rule is about direction of flow, not about `!` The single sentence worth memorising is: *a nullability change is safe when it loosens the obligation on whichever side has already shipped code.* Because arguments flow client to server and output fields flow server to client, that one rule produces two opposite answers, and candidates who memorise the output answer alone get the input case exactly backwards. ```graphql # outputs: the client READS -> may narrow what arrives certificate: Certificate -> Certificate! # safe for clients certificate: Certificate! -> Certificate # breaks clients # arguments: the client WRITES -> may widen what is accepted enrollments(sectionId: ID!) -> (sectionId: ID) # safe for clients enrollments(sectionId: ID) -> (sectionId: ID!) # breaks clients ``` Relaxing `sectionId: ID!` to `sectionId: ID` cannot invalidate a document: every client that was already sending a section id may keep sending it, and the value it sends is still accepted. Tightening the other way rejects, at validation time, every document that omitted the argument — the operation never executes at all. Input object fields behave identically, because they are the same position one level down. ## The default-value wrinkle The symmetry is not quite clean, and the wrinkle is a favourite follow-up. Under the specification an argument or input field is *required* only when its type is Non-Null **and** it has no default value. So this is legal SDL and the argument is optional to supply: ```graphql type Query { enrollments(sectionId: ID!, limit: Int! = 25): [Enrollment!]! } ``` `limit` is Non-Null — a client may never pass an explicit `null` for it — yet a document that omits it is valid, because the default fills in. Two consequences follow. First, "Non-Null argument" and "required argument" are not synonyms, and a schema diff that conflates them will misclassify edits. Second, adding a default value to an existing Non-Null argument is itself a loosening: it turns a required argument into an optional one without changing the type. ## Type-safe is not semantically safe The direction rule tells you whether documents keep *validating*. It says nothing about what the server now does with an absent value, and that gap is where the real incidents live. A course-enrolment service had a viewer-scoped field: ```graphql type Query { enrollments(viewerId: ID!, sectionId: ID): [Enrollment!]! } ``` A refactor relaxed `viewerId: ID!` to `viewerId: ID`, correctly classified as a safe input loosening, so that internal callers could omit it and let the server infer the viewer from the session. Nothing broke at validation. But a response cache sitting in front of the endpoint keyed entries on the operation plus its variables, and once `viewerId` was omitted, two different signed-in learners produced byte-identical cache keys. The second learner was served the first learner's enrolment rows. The schema change was type-safe in every sense the direction rule cares about, and it still leaked data across viewers. The lesson is not that the direction rule is wrong; it is that it is narrower than it feels. Loosening an input makes a *new absence* possible, and every layer that derives meaning from the arguments — a cache key, an authorization decision, a rate-limit bucket, an audit record — now has to answer a question it never faced. Ask what absent will mean before you make absent legal. ## Explicit null versus omitted One more distinction sits underneath all of this. Relaxing an argument to nullable makes two new situations possible, not one: the client may *omit* it, and the client may pass an explicit `null`. Under a Non-Null argument both were rejected. After the change both are accepted, and a server that treats them identically has silently decided that "I did not say" and "I say nothing" mean the same thing. Whether that is right is a design decision the type alone does not make for you. ## Answering this in an interview State the rule as one principle rather than two memorised facts, demonstrate it in both positions, then volunteer the default-value wrinkle and one semantic consequence. That progression — principle, both directions, the exception, then the thing the rule does not cover — is what separates a senior answer from a recited one.

  • Is a Non-Null argument always required in a client document?
    No. Under the specification an argument is required only when its type is Non-Null and it has no default value. `limit: Int! = 25` is Non-Null, so an explicit `null` is rejected, yet a document that omits the argument is valid because the default supplies the value. Treating "Non-Null" and "required" as synonyms misclassifies both schema edits and validation errors.
  • After relaxing an argument to nullable, what should you check beyond validation?
    Everything that derives meaning from the arguments now has a new case: response cache keys, authorization decisions, rate-limit buckets and audit records. If the server starts inferring an omitted value from ambient context such as a session, two callers can produce identical cache keys with different intended results. Decide what absent means before you make absence legal.
  • Does relaxing an argument to nullable create one new case or two?
    Two. The client may now omit the argument entirely, and it may also pass an explicit `null` — both were rejected while the argument was Non-Null. A server that folds them together has decided that "I did not say" and "I say nothing" are the same, which is a design choice the type does not make for you.

saying these in an interview costs you the question

  • Applies the output rule unchanged to arguments
  • Says adding ! to an argument is a safe tightening
  • Equates a Non-Null argument with a required argument
  • Ignores that a default makes a Non-Null argument optional
  • Assumes a validating change is automatically a safe one
  • Treats an omitted argument and an explicit null as identical

context