Validation
|
This section documents the current GraphQL specification (October 2021), plus the working draft for
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, as GraphQL tooling iterates quickly. |
Validation is the second stage of the request lifecycle: after a query document parses into a syntax tree, the server checks that tree against the schema before any resolver runs. This page catalogs the rule families a compliant server enforces, shows what a violation looks like on the wire, and points to where operation limits such as depth and complexity plug into the same phase.
Where validation sits in the request lifecycle
Getting started introduces the four-stage
pipeline every request moves through — parse, validate, execute, respond — with the diagram reproduced there
rather than repeated here. Validation is purely static: it inspects the query document’s shape against the
schema’s type system and has no access to runtime data, so every check in this page can be decided the moment a
query document and a schema are both available, without executing a single resolver.
That matters for two reasons. First, a query that fails validation never reaches execution at all — the server
returns a response with a top-level errors array and no data key, rather than a partial result. Second,
because validation only needs the query text and the schema, it is exactly the phase where a server can plug in
custom rules — most importantly depth and complexity limits — alongside the specification’s own rule set.
Security & demand control covers those custom rules in
depth; this page only marks where they attach.
The specification groups its validation rules under Section 5, Validation, organized by the part of the query document each rule family constrains: operations, fields, arguments, fragments, values, directives, and variables. The sections below walk the families that matter most in everyday schema design, each with an invalid query and the error it produces.
Anatomy of a validation error
Before looking at individual rules, it helps to see the response shape they all produce. A query that fails any
validation rule short-circuits before execution, and the server responds with one or more entries in errors,
each describing which rule was violated and where in the query document:
query GetBook($id: ID!) {
book(id: $id) {
title
isbn13
}
}
If the Book type has no isbn13 field, the server never attempts to resolve book at all — the whole
request fails validation:
{
"errors": [
{
"message": "Cannot query field \"isbn13\" on type \"Book\".",
"locations": [{ "line": 4, "column": 5 }]
}
]
}
Notice there is no data key at all, unlike the partial-success shape execution-time errors produce (covered in
Response & error handling). locations points at the
offending line and column in the query document, not at a path into the response — there is no response yet.
Every example below follows this same shape: an invalid query, then the error it triggers.
Fields must exist and resolve to a value
The most basic rule family checks that every field a query selects actually exists on the type it’s selected against, and that a field returning a scalar or enum (a leaf field) doesn’t carry a nested selection set of its own (and, conversely, that a field returning an object, interface, or union type does carry one):
query {
book(id: "42") {
title {
length
}
}
}
title is a String, a leaf type with no fields of its own to select, so nesting a selection set under it is
invalid regardless of whether length exists anywhere in the schema:
{
"errors": [
{
"message": "Field \"title\" must not have a selection since type \"String\" has no subfields.",
"locations": [{ "line": 3, "column": 5 }]
}
]
}
These checks are specified as Fields On Correct Type and Leaf Field Selections. Together they guarantee that by the time execution starts, every field in the query is known to exist on the type it’s selected against, and every leaf value the query asks for is actually a leaf.
Arguments must be known, unique, and complete
Field and directive arguments are checked against the schema’s own argument list for that field: every argument name in the query must exist on the field being called, no argument name may repeat, and every argument the schema marks as required (a non-null type with no default value) must be present:
query {
book(isbn: "978-0-000-00000-0") {
title
}
}
If book is declared in the schema as book(id: ID!): Book, the query above supplies an argument the field
doesn’t accept at all, rather than merely omitting id:
{
"errors": [
{
"message": "Unknown argument \"isbn\" on field \"Query.book\".",
"locations": [{ "line": 2, "column": 8 }]
}
]
}
Omitting id entirely produces a different, but related, error — a required argument left unsatisfied:
{
"errors": [
{
"message": "Field \"book\" argument \"id\" of type \"ID!\" is required, but it was not provided.",
"locations": [{ "line": 2, "column": 3 }]
}
]
}
These two checks are specified as
Argument Names and
Required Arguments. A closely related family,
Values of Correct Type, validates that every
argument value — literal or variable — coerces to the argument’s declared input type, catching cases such as
passing a string literal where the schema expects an Int.
Fragments must target a real, compatible type and actually be used
A named fragment (fragment BookFields on Book \{ … }) or an inline fragment must spread on a type that
exists in the schema, and that type must be possible given where the fragment is spread — an interface or
union member compatible with the surrounding selection, never an unrelated type:
fragment ReviewFields on Review {
rating
comment
}
query {
book(id: "42") {
title
...ReviewFields
}
}
ReviewFields is declared on Review, but it’s spread inside a selection on Book — and Book shares no
possible type with Review, so the spread can never produce a value:
{
"errors": [
{
"message": "Fragment \"ReviewFields\" cannot be spread here as objects of type \"Book\" can never be of type \"Review\".",
"locations": [{ "line": 9, "column": 5 }]
}
]
}
A separate rule requires the reverse relationship too: every fragment defined in a document must be spread
somewhere in it, and every fragment spread must reference a fragment actually defined in the document — an orphaned fragment ReviewFields on Review \{ … } that no operation ever spreads is just as invalid as a
…MissingFragment that spreads a name nothing defines. Finally, fragment spreads may not form a cycle
(fragment A \{ …B } / fragment B \{ …A }), since a cyclic spread would make the selection set infinite.
These three checks are specified as
Fragments On Composite Types,
Fragment spread is possible, and
Fragment spreads must not form
cycles. Variables, directives & fragments covers
fragment syntax — this page only covers the rules that make a given fragment usage legal.
Variables must be declared, typed, and used
Variables are checked from both directions: every variable a query document declares in its operation
signature (query GetBook($id: ID!)) must actually be used somewhere in that operation, and every variable a
selection set references ($id) must have been declared in the enclosing operation’s signature:
query GetBook($id: ID!, $locale: String) {
book(id: $id) {
title
}
}
$locale is declared but never referenced anywhere in the operation — a mistake most often left behind after
refactoring a query, and one static validation catches before it reaches a server that would otherwise silently
ignore it:
{
"errors": [
{
"message": "Variable \"$locale\" is never used in operation \"GetBook\".",
"locations": [{ "line": 1, "column": 26 }]
}
]
}
The inverse mistake — referencing $page without ever declaring it in the operation signature — fails for the
same underlying reason, just from the opposite direction:
{
"errors": [
{
"message": "Variable \"$page\" is not defined by operation \"GetBook\".",
"locations": [{ "line": 3, "column": 15 }]
}
]
}
A third rule checks type compatibility: a variable’s declared type must be usable everywhere it’s referenced — passing a nullable $id: ID variable into a field typed id: ID! is rejected, since the variable could
legally be null at request time while the field demands a non-null value. These three checks are specified as
All Variables Used,
All Variable Uses Defined, and
All Variable Usages are Allowed.
A related rule, Variables Are Input Types,
rejects a variable declared with an output type (an object, interface, or union) rather than a scalar, enum,
or input object — variables may only ever carry input-shaped values.
Summary of the core rule families
| Family | What it enforces | Spec section |
|---|---|---|
Fields on correct type |
Every selected field exists on the type it’s selected against. |
|
Leaf field selections |
Scalar/enum fields carry no selection set; object/interface/union fields must. |
|
Argument names & required arguments |
Arguments exist on the field/directive and required ones are present. |
|
Values of correct type |
Every argument value (literal or variable) coerces to its declared input type. |
|
Fragment validity |
Fragments target a real, compatible type; all are defined, used, and non-cyclic. |
|
Variable validity |
Variables are declared, used, of an input type, and compatible where referenced. |
The specification’s full Validation chapter additionally covers operation-level rules not detailed above — a document may define at most one anonymous operation (Lone Anonymous Operation), a subscription operation’s root selection set must have exactly one field (Single root field), and directive usage must respect each directive’s declared locations and repeatability.
Where operation limits plug in
Every rule above comes from the specification itself and runs on every compliant GraphQL server without any
extra configuration. Validation is also, by design, the phase where a server layers in rules the specification
does not mandate but that production deployments need — most importantly query depth limiting (rejecting a
query whose nested selection sets exceed a configured depth) and query complexity limiting (scoring a query
against a cost function and rejecting it above a threshold). Both are implemented as ordinary custom validation
rules that walk the same parsed, type-annotated query document the specification’s own rules walk — they run
at the same point in the lifecycle, before execution, and produce the same errors-only, no-data response
shape shown throughout this page. Security & demand
control covers how those limits are sized and enforced; this page’s contribution is only that they belong
here, alongside the specification’s own rule set, rather than in execution.
Further reading
-
graphql.org/learn/validation — the narrative introduction to validation, including the intuition behind why validation runs before execution.
-
Section 5 of the October 2021 specification — the normative rule set every example on this page draws from.
-
Execution & resolvers— what happens once a query passes every rule on this page and moves into the execute stage. -
Response & error handling— theerrorsobject shape used throughout this page, and how it differs for execution-time errors. -
Security & demand control— depth limiting, complexity limiting, and other custom validation rules that guard against expensive queries.