skip to content

Why do schema linters require a reason on every @deprecated field?

level: middleimportance: should knowfreq 46%

answer

  1. The argument has a default value
  2. The default says nothing useful
  3. Metadata that still resolves normally
  4. It reaches consumers through introspection
  5. Name the replacement and a date

basics

~10 s

Because the specification makes the argument optional and defaults it to "No longer supported", which tells a consumer nothing. The string is the only migration instruction that reaches clients, and it travels through introspection.

solid answer

~40 s

`@deprecated` is a built-in type-system directive, defined with `reason: String = "No longer supported"`. The default is what makes the lint rule necessary: writing `@deprecated` alone is legal, and the resulting string is a label rather than an instruction. That string is not decoration. Introspection exposes it as `deprecationReason` on a field and on an enum value, which is what an interactive explorer renders in its field docs and what a typed client generator can turn into a doc comment. For a consuming team in another repository, it is frequently the only notice they get. So the rule requires a non-default, non-empty reason; stricter versions require it to name the replacement field and a removal date. Be clear that this is convention: the specification is satisfied by `@deprecated` with nothing after it.

code

graphql · 11 lines
graphql
# Fails: reason falls back to "No longer supported"
type Shipment {
  carrierCode: String @deprecated
}

# Passes: names the replacement path and a removal date
type Shipment {
  carrierCode: String
    @deprecated(reason: "Use `carrier { code }`. Removed after 2027-01-31.")
  carrier: Carrier!
}

go deeper

for a junior

Be ready to say that reason is an optional argument with a default, and that a deprecated field keeps working exactly as before. Knowing the mark is metadata rather than enforcement is the level check.

for a middle

Explain the plumbing: the default value, the introspection fields that carry isDeprecated and deprecationReason, and the includeDeprecated argument that hides them unless a tool asks. Then say what a useful reason contains.

for a senior

Show that you know what the rule cannot check — that the named replacement exists, that it is equivalent, that the date has not passed — and how you close that gap with review or a registry check rather than a longer regular expression.

for a principal

Own deprecation as a communication contract across teams. Decide what a reason must contain to be actionable in your organisation, and how deprecation notices actually reach the consumers who are still selecting the field.

## What the directive actually is `@deprecated` is one of the small set of directives the GraphQL specification defines for you, applicable to field definitions and enum values — later specification work extended it to arguments and input object fields, with the restriction that a required one cannot be deprecated, since a client has no way to stop supplying it. Its definition carries a single argument with a default value: ```graphql directive @deprecated( reason: String = "No longer supported" ) on FIELD_DEFINITION | ENUM_VALUE ``` That default is the whole reason the lint rule exists. Because `reason` has one, `@deprecated` with no arguments is valid SDL, and the schema is built with the literal string `"No longer supported"` attached to the field. Nobody wrote that sentence about this field. It is a placeholder that reads like a statement. ## Where the string goes Deprecation in GraphQL is metadata, not enforcement. A deprecated field still resolves, still returns data, and a document selecting it is perfectly valid — nothing about marking it changes execution. The entire value of the mark is that it travels to consumers, and it travels through exactly one channel: introspection. The introspection system exposes `isDeprecated` and `deprecationReason` on the meta-type describing a field, and the same pair on the meta-type describing an enum value. It also hides deprecated members by default: the introspection fields that list a type's fields and a type's enum values each take an `includeDeprecated` argument that defaults to false, so a tool has to ask for them. Everything downstream reads that channel. An interactive explorer strikes the field through and prints the reason next to it. A typed client generator can emit the reason as a deprecation annotation, so the consumer's own compiler starts mentioning it. A schema registry can list every deprecated coordinate in the graph. Run that pipeline with the default string and every one of those surfaces says "No longer supported" — the field is closed, with no forwarding address. The person reading it now has to find someone on the owning team, which on a freight-tracking graph spread across eleven services usually means a message that nobody answers for two days. ## What the rule checks, and what it cannot The basic form is mechanical: `@deprecated` must carry a `reason`, and the reason must not be empty or equal to the default. That is decidable from the SDL alone, which is what makes it a schema lint rule rather than a review comment. Stricter variants add shape requirements — the reason must exceed some length, must mention a replacement, must contain a date. Each is still only a text check. A rule can insist the string names `carrier { code }`; it cannot verify that `Shipment.carrier` exists, that it returns the same value, or that the date has not already passed. Teams that care about the second half pair the rule with schema review, or with a registry check that fails when a deprecation older than the stated date is still present. ```graphql # Fails the rule: legal, but the reason is the specification's placeholder. type Shipment { carrierCode: String @deprecated } # Passes: names the replacement and when the field goes. type Shipment { carrierCode: String @deprecated(reason: "Use `carrier { code }`. Removed after 2027-01-31.") carrier: Carrier! } ``` ## Writing a reason worth requiring Three things make the difference between a reason that satisfies a rule and one that does the job. Name the replacement as a selectable path, not a prose gesture — `carrier { code }` rather than "use the carrier object". Say when the field goes, as a date, so a consumer can schedule the work instead of filing it. And say why only if the why changes what the consumer should do; "the carrier code moved onto the carrier type when we split the service" is useful, "this is legacy" is not. One thing to avoid: a reason that describes the field's fate rather than the caller's next action. "Deprecated", "do not use", "legacy field" and "see the docs" all pass a naive non-default check and leave the reader exactly where they started, which is why several teams' rules require a minimum length or a substring that looks like a field path. ## The boundary worth stating out loud None of this is specified. The specification requires no reason, no replacement, no date, and says nothing about how long a deprecated member should live or when removing it is safe — whether a removal breaks the clients that are still selecting the field is a separate question with its own tooling. What the specification gives you is a standard place to hang the string and a standard way for every consumer to read it. The convention is what turns that into a migration.

  • What happens at execution time when a client selects a field marked @deprecated?
    Nothing different. Deprecation is metadata attached to the schema; it does not change validation, execution or the response. The field resolves as it always did and no warning appears in the response envelope. That is exactly why the reason string matters — the mark is invisible to a running client, so the only way a consuming team learns anything is by reading the schema through introspection.
  • Why do deprecated fields not show up in introspection by default?
    The introspection fields that list a type's fields and enum values take an `includeDeprecated` argument defaulting to false, so a tool that just asks for fields gets the live ones. Explorers and generators opt in when they want to render the strikethrough and the reason. It means a consumer browsing a schema casually may not see a deprecated field at all until their tool asks for it.
  • Can a lint rule tell a good deprecation reason from a bad one?
    Only by shape. It can require the string to be non-empty, not the default, longer than some threshold, or to contain something that looks like a field path or a date. It cannot verify that the replacement it names exists, that the replacement returns an equivalent value, or that the date is still in the future. Those checks need the rest of the schema, a registry, or a human.

Bare @deprecated is a shop with a CLOSED sign and no forwarding address: technically informative, and useless to everyone standing outside it.

saying these in an interview costs you the question

  • Thinks @deprecated without a reason is invalid SDL
  • Says the default reason is empty rather than a placeholder string
  • Believes a deprecated field stops resolving or errors
  • Assumes consumers see the reason without introspection
  • Writes "deprecated" as the reason and calls the rule satisfied
  • Claims the specification mandates a reason string

context