Skip to content

GraphQL Validation

Lanexio™ Parser ships a first-party GraphQL validation layer inside @lanexio/parser-grammar-graphql (ADR 0050, which supersedes the ADR 0047 deferral for this deliverable). The parser separates syntax from semantics: it produces the syntax tree, not validated semantics, so validation is a pure layer over the flat AST. The layer has three pieces:

  • buildSchema(sdl) builds a GraphqlSchema object from SDL source.
  • validate(document, schema) checks an executable document against that schema with a five-rule core plus an engine depth limit.
  • toGraphqlJsDocument and validateWithGraphqlJs bridge to graphql-js for consumers who already hold a graphql-js schema. They live on the @lanexio/parser-grammar-graphql/graphql-js subpath export, are async, and load the optional peer lazily on first call; when it is absent the adapter degrades and the rest of the layer is unaffected.

The working example examples/ecosystem/graphql-validate.ts re-exports the package surface so you can copy one import path into your own project.

Validation is grammar-specific semantics: it checks a GraphQL document against a GraphQL schema. It therefore belongs in parser-grammar-graphql as a submodule, the same home pattern the HTML DOM adapter uses inside parser-grammar-html (ADR 0049). The grammar pack depends only on parser-core, so the layer adds no new dependency to your parse path, and nothing in the frozen buffer protocol changes.

buildSchema(sdl) accepts SDL as a string or UTF-8 bytes and returns { schema, diagnostics }.

import { buildSchema } from '@lanexio/parser-grammar-graphql';
const { schema, diagnostics } = buildSchema(`
enum Role { ADMIN USER }
type User { id: ID! name: String role: Role }
type Query { me: User }
schema { query: Query }
`);

The built GraphqlSchema records object, interface, input, scalar, enum, and union types. Each type carries its fields, and each field carries its declared arguments and their default values. Interfaces list what they implement, unions list their members, and possible types are resolved in a second pass, so SDL order does not matter.

Never throws. Malformed SDL (for example type { x: Int }) produces an empty or partial schema plus a diagnostics list describing what could not be understood. When the SDL omits an explicit schema { query: ... } block, buildSchema applies the GraphQL convention that a type named Query is the default query root.

validate(document, schema) accepts a parsed LexTree or raw UTF-8 bytes and returns { ok, findings }. Every finding carries the rule that fired, a human-readable message, and the byte range of the offending node, so a tool can point at the exact source span. Never throws: malformed documents and missing schema types produce findings, not exceptions.

import { parseGraphql, buildSchema, validate } from '@lanexio/parser-grammar-graphql';
const { schema } = buildSchema(`type Query { me: User } type User { id: ID! name: String }`);
const tree = parseGraphql(new TextEncoder().encode('{ me { nope } }'));
const result = validate(tree, schema);
// result.ok === false
// result.findings[0].message === 'Cannot query field "nope" on type "User".'
RuleWhat it checksGraphQL October 2021
operationAn operation resolves to a root type the schema definesSection 5.2
fieldA selected field exists on the scoped type, and sub-selections are not made on leaf (scalar/enum) typesSections 5.3.1 and 5.3.3
argumentArgument names are declared on the field and appear at most onceSection 5.4.1
fragmentSpreads reference a defined fragment, and type conditions name an existing composite typeSection 5.5
valueConst-value literals coerce to the declared type: scalar kinds, enum membership, list elements, input fields, non-nullSection 5.6
depthEngine safety limit, not a spec rule: recursive descent beyond MAX_VALIDATION_DEPTH (512) is pruned and reported instead of overflowing the call stackNone (Lanexio Parser engine bound)

The depth rule is not part of the GraphQL October 2021 ruleset. It bounds the validator’s recursive walk so that a syntactically valid but pathologically nested document yields a single depth finding rather than a stack overflow. Real documents never approach the bound.

Each spec rule is covered by a positive and a negative case in packages/parser-grammar-graphql/src/validate.test.ts, and the depth guard is covered by a 10,000-level nesting test and a value-literal-axis test.

The adapter is the only module in the package that touches graphql-js, and graphql-js is an optional peer dependency. The adapter is exported only from the @lanexio/parser-grammar-graphql/graphql-js subpath, never from the package root, so the root entry carries no graphql types and evaluating it never imports graphql-js.

import { toGraphqlJsDocument, validateWithGraphqlJs } from '@lanexio/parser-grammar-graphql/graphql-js';

Both functions are async and load the optional peer lazily on the first call through a guarded, memoized dynamic import, so the validator and type system work with graphql-js absent and importing the package never evaluates import("graphql").

  • await toGraphqlJsDocument(tree) rebuilds a graphql-js DocumentNode from the source bytes. Resolves to null when graphql-js is not installed.
  • await validateWithGraphqlJs(tree, schema) runs graphql-js validation against a graphql-js schema (for example one you built with graphql-js’s own buildSchema). Findings carry the graphql-js rule tag. When graphql-js is absent, it resolves to a single graphql-js-unavailable finding.
Terminal window
pnpm add graphql # optional: enables the adapter

Only consumers who import the graphql-js subpath need the optional peer installed. A consumer who installs @lanexio/parser-grammar-graphql without graphql-js and uses only buildSchema or validate sees the adapter disabled, never a broken validator. The adapter never throws; graphql-js parse and validation failures become findings.

examples/ecosystem/graphql-validate.ts re-exports the first-party surface, so the documentation and the package never drift apart:

import { buildSchema, validate } from './graphql-validate.js';

From the repo root:

Terminal window
pnpm vitest run examples/ecosystem/graphql-validate.test.ts
pnpm vitest run packages/parser-grammar-graphql/src/validate.test.ts

The five spec rules are an honest subset of the GraphQL validation ruleset, not full parity with graphql-js (the depth rule is the engine bound, not a spec rule). The layer deliberately does not implement fragment type compatibility with variables (5.8), directive validation (5.7), or the full FieldsOnCorrectType reporting surface. SDL type and schema extensions are also out of scope for buildSchema. For the rules it does cover, the layer is deterministic, never throws, and reports byte ranges into the original source.