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 aGraphqlSchemaobject from SDL source.validate(document, schema)checks an executable document against that schema with a five-rule core plus an enginedepthlimit.toGraphqlJsDocumentandvalidateWithGraphqlJsbridge to graphql-js for consumers who already hold a graphql-js schema. They live on the@lanexio/parser-grammar-graphql/graphql-jssubpath 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.
Why validation lives in the grammar pack
Section titled “Why validation lives in the grammar pack”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.
The type system: buildSchema
Section titled “The type system: buildSchema”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.
The validator: validate
Section titled “The validator: validate”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".'The rules
Section titled “The rules”| Rule | What it checks | GraphQL October 2021 |
|---|---|---|
operation | An operation resolves to a root type the schema defines | Section 5.2 |
field | A selected field exists on the scoped type, and sub-selections are not made on leaf (scalar/enum) types | Sections 5.3.1 and 5.3.3 |
argument | Argument names are declared on the field and appear at most once | Section 5.4.1 |
fragment | Spreads reference a defined fragment, and type conditions name an existing composite type | Section 5.5 |
value | Const-value literals coerce to the declared type: scalar kinds, enum membership, list elements, input fields, non-null | Section 5.6 |
depth | Engine safety limit, not a spec rule: recursive descent beyond MAX_VALIDATION_DEPTH (512) is pruned and reported instead of overflowing the call stack | None (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 graphql-js adapter
Section titled “The graphql-js adapter”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-jsDocumentNodefrom the source bytes. Resolves tonullwhen 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 ownbuildSchema). Findings carry thegraphql-jsrule tag. When graphql-js is absent, it resolves to a singlegraphql-js-unavailablefinding.
pnpm add graphql # optional: enables the adapterOnly 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.
Example module
Section titled “Example module”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';Run the tests
Section titled “Run the tests”From the repo root:
pnpm vitest run examples/ecosystem/graphql-validate.test.tspnpm vitest run packages/parser-grammar-graphql/src/validate.test.tsScope of this layer
Section titled “Scope of this layer”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.
Related
Section titled “Related”- DOM Adapter: the sibling first-party adapter on the flat AST
- Plugins: the plugin and visitor seams
- Adapters and extension points: the full set of extension seams
- Flat AST: the 16-byte node layout this layer reads
- parser-grammar-graphql reference
- ADR 0050: why this layer ships in the grammar pack (recorded in
docs/decisions/0050-graphql-validation.md)