skip to content

In GraphQL SDL, how do `[Skill!]!`, `[Skill]!`, `[Skill!]` and `[Skill]` differ?

level: middleimportance: must knowfreq 66%

answer

  1. Two positions, two independent promises
  2. Read the type from the inside out
  3. Inside the brackets governs the entries
  4. Outside the brackets governs the container
  5. Empty is not what a `!` forbids

basics

~20 s

Each ! governs one position, read inside out: the inner mark forbids null elements, the outer forbids a null list. So [Skill!]! allows neither null, [Skill]! allows null elements, [Skill!] a null list, and [Skill] both.

solid answer

~40 s

List and Non-Null are both **wrapping types**, so a list type is read one layer at a time from the inside out. The exclamation mark inside the brackets binds the **elements**; the one outside binds the **list itself**. That gives four independent combinations: `[Skill!]!` — the field is never null and contains no null elements; `[Skill]!` — the field is never null but individual entries may be null; `[Skill!]` — the whole field may be null, but if a list arrives none of its entries are; `[Skill]` — both the list and its entries may be null. None of the four forbids an **empty** list: `[]` satisfies every one of them, because Non-Null removes null and nothing else. Read the type aloud position by position and the four stop blurring together.

code

graphql · 12 lines
graphql
type JobPosting {
  id: ID!
  requiredSkills: [Skill!]!
  screeningAnswers: [String]!
  niceToHaveSkills: [Skill!]
  previousTitles: [String]
}

type Skill {
  id: ID!
  name: String!
}

go deeper

for a junior

Learn the reading rule before the table: the mark inside the brackets is about the entries, the mark outside is about the list. Practise saying [Skill!]! out loud as "a list that is never null, of skills that are never null".

for a middle

Explain all four combinations from the rule rather than reciting them, and be ready to state clearly that none of them forbids an empty list. Expect to be handed an unfamiliar nested type and asked what may be null where.

for a senior

Connect the declaration to what consumers experience: which shape leaves a caller unwrapping twice, and what happens to a whole list field when one element violates an inner Non-Null. Be able to spot the mismatch between a declared shape and what the backing store can actually promise.

for a principal

Own the consistency question. Left to individual authors, list shapes drift across a large schema and every consumer pays in defensive code; decide what the house rule is, where exceptions are legitimate, and how the rule is checked rather than remembered.

## Two wrapping types, and why order matters GraphQL defines exactly two wrapping types: **List**, written by surrounding a type in square brackets, and **Non-Null**, written by appending `!`. Everything else in a type reference is a named type — an object, interface, union, enum, input object or scalar. Because wrappers nest, a written type like `[Skill!]!` is not one exotic type but a stack of three: Non-Null wrapping List wrapping Non-Null wrapping the named type `Skill`. That stack is exactly how it travels over introspection, where each wrapper is a type with a null `name` and an `ofType` pointing one layer inward. It is also why the only reliable way to read a list type is **one position at a time, from the inside out**. Read `[Skill!]!` as: a `Skill`; that is never null; a list of those; that list is never null. ## The four combinations, precisely Take a job-board graph. A `JobPosting` type carries, among its 37 fields, these four: - **`requiredSkills: [Skill!]!`** — the field always appears, always as a list, and every element is a `Skill`. A caller can iterate without a null check on the list and without a null check per element. - **`screeningAnswers: [String]!`** — the field always appears as a list, but any entry inside may be null. The classic honest use is a positional list: answer three of five screening questions and the two you skipped are null at their positions. A caller must null-check each element. - **`niceToHaveSkills: [Skill!]`** — the field may be null altogether, but a list that does arrive contains no nulls. A caller checks once, at the top, then iterates freely. - **`previousTitles: [String]`** — both the list and the entries may be null. This is the fully permissive shape and it forces callers into two levels of checking. Notice that the inner and outer promises are genuinely independent: knowing one tells you nothing about the other. That independence is the whole point of the question, and it is why interviewers ask it by showing you a type rather than asking for a definition. ## Empty is not null The single most common error is reading `!` as "non-empty". It is not. `[]` — an empty list — satisfies **all four** of the types above, including `[Skill!]!`. Non-Null removes null from a set of values and does nothing else. A job posting with no required skills yet is perfectly representable as `"requiredSkills": []` under the strictest of the four types. This also means the four types give you no way to distinguish "no skills" from "we could not load the skills" if you chose the strict shape — and, conversely, that `[Skill!]` deliberately makes a null list available as a distinct signal from `[]`. Whether that distinction is worth having in a given schema is a design judgement of its own; reading the four types correctly comes first, and it is what the interview is testing. ## Deeper nesting Nothing stops the stack from being deeper. `[[Float!]!]!` — a matrix of scores, say — reads inside out as: a `Float`, never null; a list of those, never null; a list of *those*, never null. So neither the outer list, nor any row, nor any cell may be null. Change it to `[[Float!]]!` and rows may be null while cells may not; change it to `[[Float]!]!` and cells may be null while rows may not. Each bracket pair and each `!` is one independent decision, and the reading rule never changes. ## Why this shows up as a real defect The four variants translate straight into the shapes a typed client generator emits. A field declared `[String]` becomes something like an optional array of optional strings, and the calling code has to unwrap twice; a field declared `[String!]!` becomes a plain array of strings. Teams that reach for the permissive shape by reflex hand every consumer a pile of checks that will never fire, and the checks rot into casts. The mirror-image defect is worse. A field declared `[Skill!]!` whose backing store hands back a list containing a null cannot be serialized as written — the null is not permitted at that element position, so the server raises a field error instead, and because neither the element nor the list tolerates null the consequence reaches beyond the one bad entry. When a whole list field disappears from a response over a single bad row, this type is usually why. ## The habit to demonstrate In an interview, do not recite the four as a memorised table. Show the reading rule and derive them. Point at the bracket, say what the inner mark governs, point outside the bracket, say what the outer mark governs, then read the given type aloud in that order. That is the skill the question is actually probing: whether you read GraphQL types structurally or by pattern-matching on shapes you have seen before.

  • How do you read `[[Float!]!]!`, and what may be null in it?
    Inside out: a `Float` that is never null; a list of those that is never null; a list of *those* that is never null. So nothing may be null — not the outer list, not any inner list, not any cell. Drop the middle mark to get `[[Float!]]!` and inner lists may be null while cells may not. Each bracket pair and each mark is one independent decision, and the reading order never changes.
  • Which of the four shapes forbids an empty list?
    None of them. Non-Null removes null from the allowed values and nothing else, so `[]` satisfies `[Skill!]!` just as it satisfies `[Skill]`. GraphQL's type system has no way to express a minimum length, so "at least one skill" is a rule the server enforces at execution time and a caller cannot see by reading the schema.
  • A generated client type for one field is an optional array of optional values. What in the schema caused that?
    The field is declared with neither mark — `[String]` — so the generator faithfully reproduces both permissions: the field may be absent or null, and each entry may be null. That is the schema talking, not a generator quirk. If the server can genuinely always produce a list of non-null entries, tightening the declaration removes both layers of checking from every consumer at once.

Think of a crate of bottles: one label promises the crate itself will turn up, and a separate label promises no slot inside it is empty. They are stuck on different things, and either can be missing.

saying these in an interview costs you the question

  • Says a `!` on a list means the list cannot be empty
  • Reads the outer `!` as covering the elements too
  • Thinks `[Skill]!` forbids null entries inside the list
  • Believes `[Skill!]` guarantees the field itself appears
  • Treats the four shapes as one strict and three loose
  • Cannot read a nested type like `[[Float!]!]!` position by position

context