skip to content

questions

4

Why is every GraphQL field nullable by default, and what does Non-Null promise?

level: juniorimportance: must knowfreq 78%

answer

  1. Two wrapping types, both opt-in
  2. The bare type name is the permissive one
  3. Partial results need somewhere to land
  4. The exclamation mark is a forever promise
  5. Nullable value, not optional selection

basics

~20 s

In GraphQL every declared type is nullable unless it is wrapped in Non-Null, written as a trailing exclamation mark. The specification defaults to nullable so a field that cannot be produced can still return null; Non-Null promises the value is always present.

solid answer

~50 s

GraphQL's type system has only two wrapping types, List and Non-Null, and neither is applied unless you write it. So `finalGrade: String` in a course-enrolment schema declares a field the server is allowed to answer with null, and the client is obliged to handle null. Writing `finalGrade: String!` is an opt-in promise that a successful response will always carry a value there. The default is nullable rather than non-null because GraphQL is designed to serve partial results over many independent backends: nullable gives a field that could not be produced somewhere legitimate to land. Nullable is about the *value* in the response, not about whether the client must select the field — selection is always the client's choice — and not about whether an argument must be supplied, which is a separate position with its own rules.

code

graphql · 7 lines
graphql
type Enrollment {
  id: ID!
  course: Course!
  learner: Learner!
  finalGrade: String
  certificate: Certificate
}

go deeper

for a junior

Be ready to state the default out loud and point at it in SDL: a bare type name is nullable, and ! is the only way to say otherwise. Know that GraphQL has no ? and that Non-Null is a wrapper, not a keyword on the field.

for a middle

An interviewer expects you to explain both obligations the default creates — the server may return null, the client must handle it — and to separate nullability of a value from whether the client selected the field or supplied an argument.

for a senior

Show judgement about which fields earn a ! in a real schema: values guaranteed by the object's identity rather than by the current contents of a table, and be able to name a field you deliberately left nullable and say what future case you were protecting.

for a principal

Own the framing that nullability is a contract decision made once and paid for indefinitely. Be ready to say what your default posture is for a schema many teams write into, and why the permissive default is the one you can still tighten.

## The default the specification chose GraphQL's type system has exactly two *wrapping* types: List and Non-Null. Everything else — object types, interfaces, unions, enums, scalars and input objects — is nullable the instant you declare it. There is no keyword that means "nullable"; nullable is simply what you get when you write a type name on its own. Non-Null is the opt-in, spelled as a trailing `!` after the type it wraps. This is the reverse of many programming languages, where a reference is non-null-ish by convention and optionality is the annotated case, and it is the single most common source of surprise for someone reading SDL for the first time. In a course-enrolment graph the choice reads directly off the schema: ```graphql type Enrollment { id: ID! course: Course! learner: Learner! finalGrade: String certificate: Certificate } ``` `id`, `course` and `learner` are promises: any `Enrollment` the server hands back will carry all three. `finalGrade` and `certificate` are not promises — a learner halfway through a course has no grade and no certificate, and null is the honest answer. ## What nullable actually means Nullable is a statement about the *value that may appear in the response*, and it binds both sides. For the server it is a permission: returning null there is a legitimate, in-contract answer. For the client it is an obligation: code reading that position must cope with null arriving, on every response, forever. Three things nullable is *not*, and all three are common misreadings: - It is not "optional to select". Which fields a client asks for is entirely the client's choice; a Non-Null field the client did not select simply is not in the response, and that is not a violation of anything. Absence-because-not-selected and null-because-nullable are different situations that happen to look adjacent. - It is not "the field might not be implemented yet". Nullable describes the value, not the maturity of the resolver behind it. - It is not about arguments. Whether a client *must supply* an argument is decided in the input position by a different combination of rules, and the two positions behave in opposite directions when you change them. ## Why the specification defaulted this way GraphQL was designed to answer one document from many independent sources — a relational store, a search index, a third-party service — and to return whatever it could rather than nothing. Nullable-by-default is what makes that possible: when one part of the response cannot be produced, there has to be somewhere in the response shape for the absence to sit. If every field were non-null by default, an ordinary domain absence like "this enrolment has no certificate yet" would have no legal representation at all, and schema authors would have to model absence with sentinel values or extra wrapper types. The default is also the safer starting point for a schema you have to live with. Nullable is the weaker, more permissive contract, and a weaker contract can be strengthened later. Starting from the stronger one and discovering you cannot keep it is the expensive direction. ## What Non-Null promises `!` is a promise made to every current and future client, and its cost is asymmetric. To the client it is a simplification: no null check, no defensive branch, and a typed client generator can emit a plain type instead of a nullable one. To the server it is a permanent constraint: that position must always be fillable, for every object of that type, under every backend condition, for as long as the field exists. That is why `!` belongs on things guaranteed by the object's own identity — an `id`, a `createdAt`, a `Course` an `Enrollment` cannot exist without — and not on things that merely happen to be populated in today's data. The wrapper is visible in the schema itself: introspection reports it as a type of kind `NON_NULL` whose `ofType` is the wrapped type, which is exactly how schema-diffing tools and client generators see nullability rather than by parsing the `!` character. ## Reading a response ```json { "data": { "enrollment": { "id": "enr_8412", "finalGrade": null, "certificate": null } } } ``` Nothing here is anomalous. `finalGrade` and `certificate` were declared nullable, the learner is still enrolled, and null is the modelled answer. A client written against this schema must treat that response as completely ordinary — which is the whole point of the default.

  • Why did the designers not make Non-Null the default and require an annotation for nullable?
    Because a GraphQL response is assembled from many independent sources and is expected to be partial rather than all-or-nothing. Nullable-by-default guarantees there is always a legal place in the response shape for something that could not be produced. It also makes the permissive contract the starting point, which is the direction you can tighten later; a non-null default would have every schema begin with promises its author had not thought about.
  • If a client does not select a Non-Null field, is the response invalid?
    No. Non-Null constrains the *value* of a field when it is selected; it says nothing about whether the client must ask for it. Selection is always the client's choice, and a response object simply has no key for a field nobody requested. Confusing "non-null" with "always returned" is the usual beginner slip.
  • Where does Non-Null genuinely belong on an object type?
    On the values guaranteed by the object's own identity rather than by today's data: an `id`, a creation timestamp, the `Course` an `Enrollment` cannot exist without. The test is whether you can imagine any future row, backend or tenant where the value is absent. If you can, the field is nullable, because the promise has to hold for every object of that type forever.

Nullable is a blank line on a form: leaving it empty is a valid submission. Non-Null is a required field on that form, and once you print the form with it required you cannot un-require it for the copies already in people's hands.

saying these in an interview costs you the question

  • Says nullable means the client may skip selecting the field
  • Thinks a bare type name already means non-null
  • Writes GraphQL nullability as a trailing question mark
  • Marks every field Non-Null to avoid null checks in clients
  • Confuses a nullable output field with an optional argument
  • Treats a null in a nullable field as a bug by definition

context

open as a page

In GraphQL, why is making an output field Non-Null safe for clients but making it nullable again breaking?

level: middleimportance: must knowfreq 62%

basics

~20 s

Tightening an output field from a type to its Non-Null form only removes a case clients already handle, so nothing they wrote stops working. Widening it back introduces a null those clients never wrote code for, which is why that direction breaks them.

open as a page

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

level: seniorimportance: should knowfreq 46%

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.

open as a page

How would you set a nullability policy for a large GraphQL schema, given Non-Null is a one-way door?

level: principalimportance: should knowfreq 36%

basics

~20 s

Treat each Non-Null as a promise you can never withdraw without a client migration. Default to nullable, spend Non-Null only on values guaranteed by an object's identity, and make adding one a reviewed decision rather than a stylistic default.

open as a page