Last reviewed: 2026-09-13
Direct answer
GraphQL schema compatibility testing should be a required merge check with two layers. First, build the canonical candidate schema from the coding agent’s branch and compare it with the canonical schema at the pull request’s merge base. Fail the check when the comparison reports a breaking change, and require an explicit review for every dangerous change. Second, validate the repository’s maintained client operations against the candidate schema. The patch should merge only when both layers pass.
This separation matters because the checks answer different questions. A schema diff asks whether a type-system change can invalidate existing clients in general. Operation validation asks whether a known query, mutation, subscription, or fragment remains valid against this particular candidate. A clean unit-test run does not replace either check, because tests may never compile the complete schema or exercise stored operations.
Build both sides of the comparison through the same path. If the service is schema-first, compare the committed canonical schema files. If it is code-first, export the schema from a clean checkout of each revision. Do not compare an old generated schema with a new handwritten source file: formatting, omitted directives, and stale generated output can turn the result into noise.
A safe default policy is straightforward: breaking changes block; dangerous changes need review; non-breaking changes continue to operation validation. Deprecation begins a migration but does not prove that removal is safe. Keep a deprecated element until the relevant clients have migrated and the operation corpus no longer refers to it.
Who this is for
This workflow is for maintainers who let coding agents edit GraphQL type definitions, resolver signatures, schema-building code, directives, or generated schema artifacts. It is especially useful in repositories where several clients release independently, where operations are persisted or checked into source control, or where reviewers cannot infer the full public contract from the agent’s textual diff.
The operator needs access only to the repository, its normal schema build, and the client operations already approved for testing. The workflow does not require production request data. Teams without an operation corpus can still enforce the structural comparison now and add representative operations incrementally.
Key takeaways
- Compare the candidate with the pull request’s merge base, not an arbitrary local file or a moving branch tip.
- Compare canonical schemas produced by the same deterministic build path.
- Treat removals, required input additions, and incompatible type changes as merge blockers.
- Review dangerous additions such as new enum values instead of assuming every additive change is harmless.
- Validate known operations after the structural diff passes.
- Make baseline acquisition failure an infrastructure failure, never a successful compatibility result.
- Log classifications and schema coordinates, but omit operation text, variables, headers, and response data.
- Require a replacement field and a migration interval before removing a deprecated contract element.
Sources checked
- The GraphQL Specification, September 2025 Edition defines the type system, document validation, introspection, and deprecation facilities on which compatibility tooling relies. It explains that tools can validate requests against a schema before execution.
- The GraphQL schema design guidance explains continuous schema evolution, why adding types or output fields is normally non-breaking, and why non-null output types make guarantees to clients.
- The GraphQL.js utilities reference
documents
findBreakingChangesandfindDangerousChanges, including separate result categories for changes such as field removal and enum-value addition. - The GraphQL Inspector diff documentation documents old-versus-new schema comparison, breaking, dangerous, and non-breaking classifications, and a failing process result when at least one breaking change is found.
Contract details to verify
Start by defining the baseline contract precisely. For a pull request targeting origin/main, the merge base represents the common revision from which the patch diverged. This avoids comparing against unrelated commits that landed after the agent began its work. Pin the comparison tool in the repository’s dependency manifest so local and CI classifications agree.
A schema-first repository can use a small gate like this:
#!/usr/bin/env bash
set -euo pipefail
target_ref="${TARGET_REF:-origin/main}"
base_commit="$(git merge-base HEAD "$target_ref")"
work_dir='.compat'
mkdir -p "$work_dir"
git show "${base_commit}:schema/schema.graphql" > "$work_dir/base.graphql"
cp schema/schema.graphql "$work_dir/candidate.graphql"
graphql-inspector diff "$work_dir/base.graphql" "$work_dir/candidate.graphql"
For a code-first service, replace both materialization commands with the repository’s deterministic schema export. Run that export once in a clean checkout at base_commit and once at the candidate commit. If either build fails, stop before comparison and report which side failed.
Next, verify the contract implications rather than relying only on line shape:
- A removed field, type, argument, enum value, or directive can invalidate an existing document and should block by default.
- Adding a required argument or required input field can invalidate callers that do not supply it.
- Making an output more nullable weakens a guarantee that generated clients or application code may rely on.
- Adding an enum value is structurally additive but dangerous for clients that treat the enum as exhaustive.
- Changing a default argument value may preserve document validity while changing behavior, so it deserves review.
- Adding an ordinary optional output field is normally compatible because clients request fields explicitly, but the complete candidate schema must still build.
- Marking an element deprecated communicates migration intent; it does not make immediate removal compatible.
After the schema diff, validate every maintained operation against the candidate. A minimal GraphQL.js check for one complete document looks like this:
import { readFileSync } from 'node:fs';
import { buildSchema, parse, validate } from 'graphql';
const [schemaPath, operationPath] = process.argv.slice(2);
const schema = buildSchema(readFileSync(schemaPath, 'utf8'));
const document = parse(readFileSync(operationPath, 'utf8'));
const errors = validate(schema, document);
for (const error of errors) console.error(error.message);
process.exit(errors.length === 0 ? 0 : 1);
Invoke the check for each operation fixture, including any shared fragments needed to form a complete document. Keep fixture variables separate; static document validation does not need real customer values.
Happy path. The operator fetches the target ref, records the merge base, exports both schemas through identical builds, runs the structural diff, and validates every operation. The diff contains only allowed changes, all operations validate, and dangerous changes have either been ruled out or approved under the repository policy. CI writes a sanitized summary artifact and reports success on the exact candidate commit.
Error path. Suppose the diff reports FIELD_REMOVED for User.displayName. The operator confirms that the baseline and candidate were built from the intended revisions, finds the source change, and checks the operation validation output. The patch remains blocked. The coding agent restores displayName, adds a replacement field if needed, marks the old field deprecated with a useful reason, and updates tests. The operator reruns both layers on the revised commit. A failure to read the baseline, parse either schema, or load an operation is reported separately as a check error; it must not be converted into a pass.
Store enough evidence to reproduce the decision without retaining request content. A sanitized event can look like this:
{
"check_name": "graphql_schema_compatibility",
"result": "blocked",
"baseline_commit": "14bc82e1",
"candidate_commit": "7f3a91c2",
"change_class": "breaking",
"change_code": "FIELD_REMOVED",
"schema_coordinate": "User.displayName",
"operation_id": "profile-card-v2",
"error_count": 1,
"variables_logged": false
}
Recommended fields are the check name, result, baseline and candidate revisions, schema hashes, pinned tool version, change class, machine-readable change code, schema coordinate, stable operation identifier, duration, and error count. Exclude raw operation documents when a stable identifier is enough. Never record variables, request headers, response bodies, or user data in this compatibility artifact.
Failure modes
- The baseline moves during the run. Comparing directly with a branch tip before and after a fetch can produce inconsistent results. Resolve one merge-base commit and use it throughout the job.
- CI compares stale generated output. An agent may update schema-building code without refreshing a checked-in schema. Build the canonical schema in CI and fail when committed generated output differs, if the repository stores both.
- Only breaking classifications receive attention. A new enum value or changed default may be categorized as dangerous rather than breaking. Route dangerous results to review or elevate them under a documented strict policy.
- Deprecated means removable. Clients may continue selecting deprecated fields indefinitely. Require evidence that maintained operations migrated, plus the project’s normal deprecation interval, before removal.
- The operation corpus is incomplete. A green result covers only the documents supplied. Record corpus ownership and update it when a client or persisted operation is introduced.
- Fragments are validated in isolation. A fragment file may depend on definitions assembled elsewhere. Validate the same complete documents that the client build consumes.
- A missing baseline is treated as no differences. Empty or unreadable baseline files can create misleading output. Check file existence, nonzero size, parse success, and command status before accepting results.
- An exception suppresses an entire class. A global rule added for one disputed field can hide later regressions. Scope any exception to a specific schema coordinate, owner, reason, and expiry, then test that unrelated breaking changes still fail.
- Logs capture client data. Printing full operations and variables is unnecessary for schema compatibility. Prefer a stable operation identifier and schema coordinate.
- The agent repairs the symptom by weakening policy. A patch that changes the CI rule instead of restoring compatibility needs separate review. Protect the workflow and policy files with ownership rules.
FAQ
Is every new GraphQL field safe?
An ordinary optional output field is normally non-breaking because clients select fields explicitly. That does not guarantee the resolver works, the schema build succeeds, or repository-specific rules pass. New required input fields are different: existing callers may omit them, so compatibility tools classify them as breaking.
Can a deprecated field be removed as soon as the diff tool allows an override?
The override changes gate behavior, not client reality. Keep the field until the migration window is complete and the maintained operation corpus no longer selects it. Document narrow exceptions rather than making deprecated-field removal globally non-blocking.
Should dangerous changes always fail CI?
That depends on the client contract. A strict repository may fail all dangerous changes. Another may require a reviewer to approve enum additions or default changes. What matters is that dangerous results remain visible, machine-readable, and subject to a written policy.
Why validate operations after running a schema diff?
The schema diff classifies structural risk across the contract. Operation validation identifies which known documents are invalid against the candidate. Together they provide both a broad guardrail and concrete repair evidence.
What if the service generates its schema from application code?
Export the schema at both revisions in isolated, reproducible environments. Use identical dependency and build settings, then compare the exported artifacts. Comparing current generated output with a historical handwritten file does not test one coherent contract.
What should happen when the compatibility tool crashes?
Return a distinct check error and block the merge. Preserve the command status, tool version, and sanitized diagnostic. Do not reinterpret an incomplete comparison as zero breaking changes.
Reader next step
Choose one canonical schema build and add two fixtures today: one additive change that must pass and one field removal that must fail. Then add one representative operation and require the compatibility job on pull requests that touch schema sources, build logic, directives, or operation fixtures.
Give the coding agent explicit acceptance criteria using the reviewable task-brief workflow . Record the baseline, schema coordinates, intended migration, and excluded files in a change scope note . If the new gate exposes unrelated failures, use a bounded CI repair loop and keep policy changes separate from schema repairs.
The first successful rollout is not a permanently green check. It is a gate that reliably proves the safe fixture passes, the breaking fixture fails, baseline errors block, and the resulting evidence tells a maintainer exactly what to repair.