skip to content

Why does a schema lint rule flag the nullable item type in a [Leg] field?

level: middleimportance: should knowfreq 39%

answer

  1. Two independent modifier positions
  2. What would a null element mean here
  3. The optional the consumer can never remove
  4. One failing element, the whole list gone
  5. A default with a documented exception

basics

~20 s

Because [Leg] permits a null in every element slot, and a null element has no natural meaning in a collection. Every consumer must branch on a hole nobody put there on purpose, so linters push for [Leg!].

solid answer

~40 s

In GraphQL SDL the list modifier and the item modifier are independent. `[Leg]` means the list may be null and any element may be null; `[Leg]!` means the list is always present but elements may still be null; `[Leg!]` forbids null elements; `[Leg!]!` forbids both. Almost nobody intends a null element — "an unknown leg" is not a thing a freight-tracking graph can return — yet the nullable item leaks into every consumer, because a typed client generator maps it to an optional and the branch survives forever. Hence the rule. It has a genuine counterargument, though: with `[Leg!]`, a field error while resolving one element cannot be represented in place, so the null propagates outward and takes the whole list with it. Teams that want per-element partial results keep the item nullable deliberately.

code

graphql · 6 lines
graphql
type Shipment {
  a: [Leg]     # list may be null; any element may be null
  b: [Leg]!    # always a list; any element may be null
  c: [Leg!]    # list may be null; no element is ever null
  d: [Leg!]!   # always a list; no element is ever null
}

go deeper

for a junior

Be able to read all four list shapes out loud and say which position each exclamation mark constrains. That alone is what a screening question here is checking.

for a middle

Explain why a null element has no domain meaning and what it costs a consumer, then show you know the modifier positions are independent decisions rather than one setting.

for a senior

Bring the counterargument: a non-null item means one failing element propagates a null outward and erases the list, so the rule needs a documented exception for fields that want per-element partial results.

for a principal

Own the default and the escape hatch together. Decide whether your graph optimises for consumer simplicity or partial availability, write that decision down, and make the linter encode it rather than relitigating it per pull request.

## Two modifiers, two decisions A GraphQL list type wraps an inner type, and non-null is a separate wrapper that can sit in either position. That gives four shapes for a field returning legs of a shipment, and they are four different promises: ```graphql type Shipment { a: [Leg] # the list may be null; any element may be null b: [Leg]! # always a list; any element may be null c: [Leg!] # the list may be null; no element is ever null d: [Leg!]! # always a list; no element is ever null } ``` The lint rule under discussion is only about the inner position. Whether the list itself should be non-null is a separate argument, and a good answer keeps them apart — conflating them is the most common slip on this question. ## Why a null element is almost always a mistake Ask what a null in position two of `legs` would mean. In a freight-tracking graph, a shipment has an ordered sequence of legs; there is no such thing as a leg that exists in the itinerary but has no identity. The null is not a domain value. It is either a bug or an error hole, and in neither case does the schema help the reader decide which. The cost is paid downstream and paid repeatedly. A typed client generator maps a nullable item to an optional element type, so every piece of consumer code that walks the list carries a check that is never satisfied by real data and never removable, because the schema says it might be. Multiply that by the number of list fields in a large graph and it is a meaningful, entirely self-inflicted tax. That is precisely the sort of cost a mechanical rule is good at preventing, because it is invisible in the pull request that introduces it and expensive to unwind later. ## The counterargument, which is real The rule is a default, not a law, and a candidate who states it as absolute is missing the interesting half. When a resolver raises a field error, the specification's error handling puts null in that field's position and records an entry under `errors`. If the position is declared non-null, null cannot go there, so the failure propagates outward to the nearest nullable ancestor. With `legs: [Leg!]`, a field error while resolving one element cannot be represented at that element, so the entire list becomes null — one bad leg erases the other nine, and the client sees no legs at all rather than nine plus an error entry. This is not theoretical, and it is the shape of failure that shows up only in production: a per-leg lookup that is instant against seed data starts timing out against real volumes, the resolver raises on one element, and a field that had been returning fine for a year returns null for the whole list. Teams that genuinely want per-element partial results therefore choose the nullable item on purpose, and pair it with an error entry whose path points at the failing index. The rule should be configured to allow that with a documented exception, not argued with case by case. ```json { "data": { "shipment": { "legs": [ { "id": "leg-8", "arrivedAt": "2026-04-02T11:07:00Z" }, null, { "id": "leg-10", "arrivedAt": null } ] } } } ``` Notice that this response has two different nulls in it, and the schema is what tells them apart. The element null is only possible because the item type is nullable. The `arrivedAt` null is a field that is legitimately absent — the leg has not arrived. A reader cannot infer either from the JSON alone. ## What the rule cannot decide for you Three things stay with the author. Whether the list itself should be non-null: `[Leg!]` still lets the whole field be null, which is often what you want when the parent may not have legs at all, and `[Leg!]!` commits you to returning at least an empty array in every circumstance including failure. Whether an empty list and a null list mean different things in your domain, and if so which one means what — non-null on the list forecloses that distinction, which is sometimes exactly the point. And whether the element type should be an object at all; a list of union members, for instance, brings a different set of consumer costs that this rule says nothing about. One clarification worth making early in an answer, because it is a frequent misconception: a non-null item type says nothing about the list's length. `[Leg!]!` permits an empty array. Non-null constrains what a value may be, never how many values there are, and GraphQL has no way in the type system to require a non-empty list.

  • When is a nullable item type the right choice rather than a lint violation?
    Two cases. When the field is a positional batch lookup — the caller passes a list of ids and expects a result slot per id, where a null marks the id that was not found and position carries the correspondence. And when the team wants per-element partial results, so that one failing element leaves a null in place and an error entry pointing at its index instead of erasing the whole list. Both are deliberate, and both deserve a comment in the SDL.
  • Does the rule say anything about whether the list itself should be non-null?
    No, and keeping the two apart matters. `[Leg!]` still permits the whole field to be null, which is reasonable when the parent may genuinely have no legs to report. `[Leg!]!` commits the server to returning at least an empty array under all circumstances, and it also removes the option of using null and empty to mean different things.
  • Does a non-null item type guarantee the list has at least one element?
    No. `[Leg!]!` permits an empty array, and there is no type-system construct in GraphQL for requiring a non-empty list. Non-null constrains what a value may be, never how many values there are. If emptiness is meaningful in your domain, that has to be documented or expressed some other way — a count field, or a union that names the empty case.

A nullable item type is a manifest that permits blank lines: every reader has to decide what a blank line means, and nobody ever writes one deliberately.

saying these in an interview costs you the question

  • Thinks [Leg] already forbids null elements
  • Says [Leg]! makes the elements non-null too
  • Claims the specification requires non-null list items
  • States the rule as absolute with no counterargument
  • Believes [Leg!]! guarantees a non-empty list
  • Cannot say what a null element would mean to a caller

context