In GraphQL introspection, why is __Type.name null for a field typed [Artwork!]!, and how do you read it?
answer
- Named types and wrapping types are both __Type
- Only two kinds have no name
- Follow the pointer downwards
- Render outside-in, unwrap inside-out
- Documents cannot recurse, so unroll
basics
~20 sThat field's type is not one type but a chain of __Type records: NON_NULL wrapping LIST wrapping NON_NULL wrapping the object type. Only the innermost, named record carries a name, so a consumer walks ofType down to it.
solid answer
~40 s`__TypeKind` has eight values. Six are **named** kinds — `SCALAR`, `OBJECT`, `INTERFACE`, `UNION`, `ENUM`, `INPUT_OBJECT` — which carry a `name` and a null `ofType`. Two are **wrapping** kinds, `LIST` and `NON_NULL`, which carry a null `name` and point at what they wrap through `ofType`. So `[Artwork!]!` introspects as four nested records and only the innermost has a name; reading `field.type.name` at the top gives null for every wrapped field in the schema. A consumer needs two walks: one that renders the type outside-in, appending `!` for `NON_NULL` and bracketing for `LIST`, and one that descends to the named type. A further consequence: an executable document cannot recurse, so introspection queries hand-unroll `ofType` a fixed number of levels — commonly about seven — and anything deeper is silently truncated to null.
code
json · 13 lines{
"name": "artworks",
"type": {
"kind": "NON_NULL", "name": null,
"ofType": {
"kind": "LIST", "name": null,
"ofType": {
"kind": "NON_NULL", "name": null,
"ofType": { "kind": "OBJECT", "name": "Artwork", "ofType": null }
}
}
}
}go deeper
Know that a field's type in introspection is not always a plain name: lists and non-null are represented as wrappers around the real type, and you follow ofType to find it.
Explain the eight __TypeKind values, which two are wrappers, and how [Artwork!]! decomposes into four records. Be able to write the short recursive renderer that turns a chain back into type text.
Diagnose the classic symptom — a tool reporting a field's type as null — straight to the missing unwrap, and know why the unroll depth is fixed, what truncation looks like, and why the wrapper design beats boolean flags for nested lists.
Own the consequence for anything the organisation builds on introspection: a shared unwrapping helper rather than one per tool, an explicit error on truncated chains, and a stance on how much of the type system consumers should have to reimplement.
## Two kinds of type, one record shape GraphQL's type system has **named types** — scalars, objects, interfaces, unions, enums and input objects — and **wrapping types**: list and non-null. Introspection represents both with the same `__Type` record, and tells them apart with the `kind` field, whose value comes from the `__TypeKind` enum: `SCALAR`, `OBJECT`, `INTERFACE`, `UNION`, `ENUM`, `INPUT_OBJECT`, `LIST`, `NON_NULL`. The first six are named. The last two are not: a list of somethings has no name of its own, and neither does "a something that cannot be null". So for a `LIST` or a `NON_NULL` record, `name` is `null` and `ofType` points at the type being wrapped. For every named kind the reverse holds: `name` is set and `ofType` is `null`. That is the whole answer to why `name` comes back null. A field declared `[Artwork!]!` is not one type called `[Artwork!]!`; it is four nested records, and only the innermost one has a name. ```json { "name": "artworks", "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "Artwork", "ofType": null } } } } } ``` Read outside-in, that says: non-null list, of non-null `Artwork`. Every consumer of introspection — anything that prints a type, generates code, renders documentation or compares two schemas — must walk that chain rather than read `name` at the top. Code that reads `field.type.name` and prints it works fine on the handful of bare, nullable fields in a schema and produces `null` everywhere else, which is exactly the shape of the bug reports this generates. ## The unwrapping algorithm ```pseudocode function renderType(t): if t.kind == "NON_NULL": return renderType(t.ofType) + "!" if t.kind == "LIST": return "[" + renderType(t.ofType) + "]" return t.name function namedTypeOf(t): while t.ofType != null: t = t.ofType return t ``` Two helpers, and almost every real consumer needs both: one to display the type faithfully, one to answer "which named type is ultimately involved here?" — the question you ask when resolving references between types. ## Why introspection queries unroll a fixed depth Here is the part that catches people. The chain is recursive, but a GraphQL **executable document is not**: a selection set is written out literally, and a named fragment may not spread itself, directly or transitively. There is no way to write "follow `ofType` until it is null". So an introspection query hand-unrolls the chain a fixed number of levels — the canonical shape used across the ecosystem unrolls about seven — and anything nested deeper than the unrolled depth is truncated to `null`. ```graphql query FieldTypes { __type(name: "Gallery") { fields { name type { kind name ofType { kind name ofType { kind name ofType { kind name } } } } } } } ``` Seven levels is not an arbitrary comfort margin. Wrapper nesting is bounded in practice because `NON_NULL` may not wrap `NON_NULL` — `Artwork!!` is not a valid type — so the chain only lengthens when lists nest. A bare named type is one record, `[Artwork!]!` is four, and a doubly nested `[[Artwork!]!]!` is six. Seven levels therefore covers three levels of list nesting, which no sane schema exceeds. But the truncation is silent: a consumer that hits the floor sees a `__Type` with a null `name` and a null `ofType` and cannot tell that apart from a malformed response, so a type-rendering routine should treat that state as an explicit error rather than printing an empty string. ## Why the representation is shaped this way The alternative design — a named type plus boolean `isList` and `isNonNull` flags — cannot express the distinction GraphQL cares most about. `[Artwork]!`, `[Artwork!]` and `[Artwork!]!` are three different contracts about where a null may appear: in the list, instead of the list, or nowhere. Two booleans give you four combinations and no way to talk about the *inner* nullability of a nested list. The wrapper chain expresses arbitrary nesting exactly, at the cost of making every consumer write the loop. ## Watch for * Reading `name` off a field's type and reporting the server as broken when it is null. * Believing `[Artwork!]!` is a single named type somewhere in `__schema.types`. * Expecting to traverse `ofType` recursively in one document. * Thinking `NON_NULL` can wrap another `NON_NULL`. * Rendering the chain inside-out, and producing `[Artwork]!!` or similar nonsense.
- How deep do practical introspection queries unroll ofType, and why not just recurse?About seven levels. A GraphQL executable document is written out literally and a named fragment may not spread itself, so there is no way to express "follow ofType until null". Seven covers three levels of list nesting, which is far beyond any real schema — but the truncation is silent, so a renderer should treat a null name with a null ofType as an error.
- Which __TypeKind values have a null name, and which have a null ofType?LIST and NON_NULL have a null `name` and a non-null `ofType`. The six named kinds — SCALAR, OBJECT, INTERFACE, UNION, ENUM, INPUT_OBJECT — have a `name` and a null `ofType`. The two states are mutually exclusive, which is what makes the walk terminate.
- Can a NON_NULL record wrap another NON_NULL record?No. A non-null type's inner type may not itself be non-null — there is no `Artwork!!` — so wrappers only stack through list nesting. That bound is why a fixed unroll depth is safe: the chain grows by two records per additional list level, not arbitrarily.
It is like nested parcels: the outer boxes are labelled only "do not open" and "contains several", and the sender's name is written on the smallest box at the centre.
saying these in an interview costs you the question
- Reads name off a wrapped type and blames the server
- Thinks [Artwork!]! is one named type in the type list
- Expects to traverse ofType recursively in one document
- Treats list and non-null as booleans on a named type
- Renders the chain inside-out, producing invalid type text