skip to content

What does committing a GraphQL schema's printed SDL as a CI snapshot actually catch?

level: middleimportance: nice to knowfreq 31%

answer

  1. Make an invisible change visible
  2. Most useful when SDL is generated
  3. The diff is the product
  4. Unstable print order ruins the diff
  5. Changed is not the same as breaking

basics

~20 s

Any change to the schema's printed shape, turned into a reviewable diff. It is most valuable with a code-first schema, where SDL is a byproduct of code and a rename or a nullability change is otherwise invisible in review.

solid answer

~50 s

The job is mechanical: build the schema the server would serve, print it to SDL, and diff that text against a file committed in the repository. CI fails when they differ, and the fix is to review the diff and commit the new file. Its whole value is visibility. With a code-first schema the SDL is generated from code, so renaming a property, changing a return type's nullability or adding a directive application changes the public contract without any obvious sign in the diff a reviewer reads — the snapshot makes that change the diff. Two caveats matter in an interview. The printed output must be deterministic, or unrelated refactoring produces enormous no-op diffs. And a snapshot only says the schema changed; whether the change breaks existing clients is a different check against real client documents or a registry.

code

graphql · 15 lines
graphql
type Bin {
  aisle: String!
  code: ID!
  pallets: [Pallet!]!
}

type Pallet {
  id: ID!
  quantityOnHand: Int!
  sku: String!
}

type Query {
  bin(code: ID!): Bin
}

go deeper

for a junior

Know what the check is: the schema is printed to SDL and compared with a file in the repository, and a failure means the schema's shape changed since that file was committed.

for a middle

Explain why it earns its place with a code-first schema, and name the determinism requirements — sorted output, normalized descriptions, a pinned build configuration — that keep the diff meaningful.

for a senior

Show that you know its limit: it detects change, not breakage. Say which separate signal you use to judge safety, and how you stop regeneration from becoming a reflex that hides a real diff.

for a principal

Weigh this against a registry-backed check for your organisation: a repo-local snapshot is cheap and reviewer-facing, while a published-schema check knows about consumers the repository cannot see.

## The mechanic A schema snapshot test has three steps and no cleverness. Build the executable schema exactly as the server builds it. Print it to SDL — the textual schema definition language, with types, fields, arguments, default values, deprecations and directive applications. Compare that string against a file checked into the repository, commonly something like `schema.graphql`. If they differ, the build fails, and the developer's remedy is to look at the diff, decide the change was intended, and commit the regenerated file. Print from the **built schema**, not from an introspection result. Introspection deliberately does not report directive applications, so a schema reconstructed from introspection silently loses every `@deprecated`, every federation directive, and every custom type-system directive you applied — exactly the parts you most want a diff to show. ## Why the code-first case is the whole point If your schema is authored as SDL and committed as SDL, the SDL file *is* the diff, and a snapshot adds little. The practice earns its keep with a code-first schema, where the schema is derived from types, classes or builder calls. There the public contract is a side effect of code, and the mapping is easy to change without noticing. In a warehouse inventory graph, renaming an internal property `qtyOnHand` to `quantityOnHand` reads in review as a tidy-up; if the field name is derived from the property name, it just renamed a public field. Making a nullable return type non-null, or the reverse, moves `Int` to `Int!` with no visible schema edit. Registering a new type for internal reuse can attach a whole subtree to the public schema. A snapshot converts each of those into a line a reviewer sees. ## Determinism, and the ordering assumption that breaks The failure mode teams actually hit is noise. A code-first builder typically prints types in the order they were registered, which is an accident of module load order. A warehouse graph whose printed schema ran to 3,182 lines had its type registrations moved into a per-domain module during a refactor; nothing about the contract changed, and the snapshot diff came back at 900-odd lines of pure reordering. Two of those lines were a real nullability change, and they were approved along with everything else. A snapshot that produces large diffs for no reason trains reviewers to approve snapshot diffs. So print deterministically: - sort types, fields, arguments, enum values and directive applications by name; - normalize descriptions and whitespace so a reflowed comment does not move lines; - pin the build to one configuration — a schema whose fields depend on feature flags or environment must be snapshotted with those inputs fixed, or the file flips depending on who ran it; - keep generated values out. A schema that embeds a build timestamp or version string in a description guarantees a diff on every run. ```pseudocode built = buildSchema(application) printed = printSchema(built, sortTypes: true, sortFields: true) expected = readFile("schema.graphql") if printed != expected: writeFile("schema.actual.graphql", printed) fail("Schema changed. Review the diff, then commit the regenerated file.") ``` ## What the snapshot does not tell you A snapshot is a change detector, not a policy. It fails identically for a purely additive new field, a renamed field that breaks every client, and a reordering caused by a printer upgrade. It has no opinion, because it has no idea what any client selects. That means it cannot answer the question people often expect of it — *is this change safe to ship?* Answering that needs a different input: the documents real clients execute, or a registry that knows which fields have been used recently. Deciding what counts as a breaking change and what the removal policy is belongs to schema-design work, not to the snapshot. Nor does the snapshot check quality: a field with no description, a `@deprecated` with no reason, or a nullable item inside a list all pass, because the text matched. Those are rules a schema linter evaluates. It also proves nothing about behaviour. The schema can print perfectly while every resolver behind it throws. ## How to treat the failure Because the snapshot fails on *any* change, its value depends entirely on the review ritual around it. Regenerating the file must be a deliberate act with the diff read, not a command someone runs reflexively to get green. Some teams put the schema file in a path with a required reviewer so the diff cannot merge unnoticed. That is the real product of the practice: not the file, but the fact that a change to the public contract can no longer reach production without a human having looked at it.

  • Your snapshot diff is 900 lines after a refactor that changed no field. What went wrong and how do you fix it?
    The printer emitted types in registration order, and the refactor changed load order. Nothing about the contract moved, but the diff is unreadable, so a real change hiding in it gets approved. Fix it by printing deterministically — sort types, fields, arguments, enum values and directive applications by name, normalize whitespace and descriptions — then regenerate the committed file once so subsequent diffs are semantic only.
  • Why print from the built schema rather than reconstructing SDL from an introspection response?
    Introspection does not report directive applications. Reconstructing SDL from it silently drops every `@deprecated` with its reason and every custom or composition directive applied to types and fields, so the snapshot would show no diff when exactly those change. Printing the schema the server actually built keeps them.
  • The snapshot fails on a purely additive field. Does that mean the check is too noisy?
    No — that is the check working as designed. It detects change and expresses no opinion about safety, because it does not know what any client selects. The remedy is to keep the diff small and readable so an additive change is obviously additive, and to answer the safety question with a separate check against real client documents or a usage-aware registry.

saying these in an interview costs you the question

  • Treats a snapshot failure as proof clients will break
  • Regenerates the file without reading the diff
  • Reconstructs the SDL from introspection and loses directives
  • Prints in registration order and accepts huge diffs
  • Expects the snapshot to catch missing descriptions
  • Believes a matching snapshot means resolvers work

context