Validation

This section documents the current GraphQL specification (October 2021), plus the working draft for @defer/@stream/@oneOf, and the GraphQL-over-HTTP specification, as published at graphql.org and spec.graphql.org — which are the references these pages are written and verified against — and, for the integration pages, against Spring for GraphQL (2.0.x), Strawberry, Ariadne, and the client docs for Apollo Client / urql / Relay. No specific patch version is pinned. Some surfaces (Apollo Router/managed federation, graphql-ws internals, the Relay compiler internals, GraalVM native) are linked rather than documented in depth.

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.

5.3.1

Leaf field selections

Scalar/enum fields carry no selection set; object/interface/union fields must.

5.3.3

Argument names & required arguments

Arguments exist on the field/directive and required ones are present.

5.4.1

Values of correct type

Every argument value (literal or variable) coerces to its declared input type.

5.6.1

Fragment validity

Fragments target a real, compatible type; all are defined, used, and non-cyclic.

5.5.1

Variable validity

Variables are declared, used, of an input type, and compatible where referenced.

5.8.4

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