What does a schema linter flag in SDL that is still legal GraphQL?
answer
- Two gates, only one refuses to start
- Legal is not the same as intended
- The spec is quiet about a lot
- Descriptions, reasons, list items, dead types
- Convention enforced at build time
basics
~20 sConventions the specification deliberately leaves open: a missing description, an @deprecated with no reason, a nullable item inside a list, a type no root field can reach. All of that is a valid schema; a linter objects anyway.
solid answer
~50 sThere are two separate gates on a schema. The specification's type-system validity rules are the hard one: an object type with no fields, a type that claims an interface without declaring all of its fields, a duplicated field name, a directive applied where its definition does not allow it. Fail those and there is no schema to serve. A schema linter runs entirely above that line, on a schema that already builds. Its rules encode house conventions the specification says nothing about — a description on every publicly reachable field, a real `reason` on every `@deprecated`, `[Leg!]` rather than `[Leg]`, no type stranded out of reach of the root operation types. In an interview, say which side of the line a rule sits on. Calling a lint rule "the spec" is the mistake that gets noticed.
code
graphql · 19 linestype Query {
shipment(id: ID!): Shipment
}
type Shipment {
id: ID!
legs: [Leg] # nullable item inside a list
carrierCode: String @deprecated # no reason argument
}
type Leg {
id: ID!
arrivedAt: String # no description
}
type CarrierManifest { # unreachable from any root field
id: ID!
sealNumber: String!
}go deeper
Be ready to name two or three concrete rules and say plainly that each one is a convention, not a specification requirement. Knowing that a lint failure never stops the server from serving the schema is the point being checked.
Explain the two gates precisely: what type-system validity actually rejects, versus what a linter only has an opinion about, and why the second list is where team decisions live.
Show judgement about which conventions are worth mechanising at all, and be honest about what linting cannot see — accuracy, granularity, authorization, cost. Say where in the build the check belongs and on which artefact.
Own the boundary between what a machine enforces and what schema review decides. Argue for a small set of rules with a real downstream cost behind each, rather than a large ruleset that trains people to skim past it.
## Two gates, and only one of them can stop the server Every GraphQL server runs a schema through one mandatory check before it will serve a request. The specification calls it type-system validation, and it asks structural questions with yes-or-no answers: does the query root type exist and is it an object type; does every object type declare at least one field; does a type that claims to implement an interface declare every one of that interface's fields with a compatible type and compatible arguments; are field names unique within a type; does every type named in a field or argument position actually exist; is every directive applied in a location its definition permits, and applied once where the definition is not repeatable. Fail any of these and there is no schema. The server refuses to build it, and there is nothing to lint because there is nothing to serve. That is a different check from validating an operation a client sends. Document validation happens per request, against a schema that is already known good. Schema linting is neither: it is a build-time opinion about a schema that has passed the first gate and would happily serve traffic. ## What is left for the linter Everything a linter flags is, by construction, legal. The specification is deliberately quiet about a large surface, and a linter fills that silence with a team's decisions. The four rules most commonly seen in the wild are all in this category: - **A description on every publicly reachable field.** Descriptions in SDL are string literals written before a definition, exposed to consumers through introspection's `description` field. The specification requires none of them anywhere. - **A real `reason` on every `@deprecated`.** The built-in directive defines `reason` with a default, so bare `@deprecated` is legal and produces a string that tells a consumer nothing. - **No nullable item type inside a list.** `[Leg]` permits a null in every element slot. `[Leg!]` does not. Both are valid; only one of them is what almost every author meant. - **No type unreachable from the root operation types.** A type that no field returns and no union or interface admits is still a type. It is still shipped through introspection to every consumer. ```graphql # Every line here passes type-system validation. type Query { shipment(id: ID!): Shipment } type Shipment { id: ID! legs: [Leg] # lint: a null may appear in any element slot carrierCode: String @deprecated # lint: no reason argument given } type Leg { id: ID! arrivedAt: String # lint: no description on a public field } type CarrierManifest { # lint: no root field can reach this type id: ID! sealNumber: String! } ``` A freight-tracking graph that shipped exactly this SDL would run. Every consumer of it would also inherit four small, permanent taxes: a null-check per leg, a deprecation notice with no forwarding address, an undocumented timestamp field of unstated timezone and format, and a type in the generated client that nothing can ever return. ## Why the distinction is itself the interview question A great deal of what people call "GraphQL" is convention. Connections and cursors, global object identification, persisted documents, cost limits, and every rule in this list are widespread practice, not specified requirements. Candidates who cannot separate the two make two characteristic mistakes. They assert that the specification forbids something it permits, which makes every downstream claim suspect. And they treat a lint failure as a correctness failure, which leads to arguing about a nullable item type as though the schema were broken rather than as a tradeoff with a real counterargument. The useful framing is about who pays. A validity failure is paid by the team that wrote it, immediately, at startup. A lint violation is paid by everyone downstream, slowly: the consumer reading undocumented field, the generated client carrying an optional it never needed, the team that deletes a deprecated field and discovers nobody was told what to move to. Linting exists because none of those people are in the pull request. ## What linting cannot see A linter reads the SDL and nothing else. It can check that a description exists; it cannot check that the description is true, current, or useful. It can check that a `reason` string is present and not the default; it cannot check that the replacement it names exists. It can check the shape of a type modifier; it cannot tell whether a nullable field is a considered decision or an oversight. It knows nothing about behaviour, authorization, resolver cost, or whether the mutation you added is the right granularity. A clean lint run is a floor. It is evidence that the obvious mechanical conventions hold, and it is not evidence of design quality — which is why teams pair it with schema review rather than replacing review with it.
- Name something the specification's validity rules reject that no linter needs a rule for.An object type that declares the same field name twice; a type that says it implements an interface but omits one of that interface's fields, or narrows the field's type incompatibly; a schema whose query root is not an object type; a directive applied in a location its definition does not list. None of these is a convention — the result is not a schema at all, so the server never gets far enough to lint it.
- Does a schema that passes every lint rule tell you the API is well designed?No. Linting checks only what is decidable from the SDL text: the presence of a description, the presence of a deprecation reason, the shape of a type modifier. It cannot tell whether the description is accurate, whether a mutation is the right granularity, or whether a field returns what its name implies. Treat a clean run as a floor and keep human schema review for the rest.
- Where in the pipeline does schema linting normally run, and on what artefact?At build time, against the printed SDL of the schema the server actually constructed, rather than against hand-written source files. That matters for a code-first schema, where the SDL only exists once the server has built it, and for a composed graph, where the artefact worth linting is the composed schema rather than any one service's contribution.
Type-system validation is the building code: a structure that fails it cannot be occupied. A linter is the practice's own style guide on top — everything it objects to would stand up perfectly well.
saying these in an interview costs you the question
- Says a lint violation makes the schema invalid GraphQL
- Claims the specification requires descriptions on fields
- Thinks the server refuses to start on a lint failure
- Confuses schema linting with per-request document validation
- Cannot name one rule that is convention rather than specification
- Assumes a clean lint run means the schema is well designed