skip to content

What does @deprecated(reason:) do in a GraphQL schema, and which locations may carry it?

level: middleimportance: must knowfreq 64%

answer

  1. Schema metadata, not runtime behaviour
  2. Four locations, two added in 2021
  3. Required arguments may not carry it
  4. Introspection hides it by default
  5. includeDeprecated brings it back

basics

~20 s

@deprecated is a built-in type-system directive marking a field definition or enum value as retired — and since the October 2021 edition an argument or input field too. It changes nothing at execution; it hides the member from introspection by default.

solid answer

~50 s

`@deprecated` is one of the specification's built-in type-system directives, applied in the schema rather than in a client's document. Its definition is `directive @deprecated(reason: String = "No longer supported") on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE` — the last two locations arrived with the October 2021 edition; before that only output field definitions and enum values could be marked. It has no effect on validation or execution: a document that selects a deprecated field is perfectly valid and the resolver runs exactly as before. Its real effect is on introspection. `__Type.fields`, `__Type.enumValues`, `__Type.inputFields` and `__Field.args` each take `includeDeprecated: Boolean = false`, so deprecated members are omitted from the default listing, while `isDeprecated` and `deprecationReason` expose the mark to anyone who asks for it. One type-system rule to know: a required argument or input field — non-null with no default — must not be deprecated, because a caller has no way to stop sending it.

code

graphql · 28 lines
graphql
scalar Money

enum StatementChannel {
  ONLINE
  POST
  FAX @deprecated(reason: "Withdrawn; use POST.")
}

input StatementFilter {
  accountId: ID!
  channel: StatementChannel
  paper: Boolean @deprecated(reason: "Use channel: POST.")
}

type Transaction {
  id: ID!
  amount: Money!
}

type Statement {
  id: ID!
  closingBalance: Money!
  balance: Money! @deprecated(reason: "Ambiguous mid-period; use closingBalance.")
  transactions(
    first: Int = 50
    pageSize: Int @deprecated(reason: "Use first.")
  ): [Transaction!]!
}

go deeper

for a junior

Know that @deprecated is written in the schema next to a definition, that it takes a reason string, and that the field keeps working exactly as before once it is marked.

for a middle

Be able to recite the four locations and note that arguments and input fields joined in the October 2021 edition, then explain the introspection behaviour: hidden by default, revealed with includeDeprecated, with isDeprecated and deprecationReason alongside.

for a senior

Show you know the directive is advisory and buys nothing on its own. Talk about what a reason should contain, the two-step dance for a required argument, and why deprecating every returning field is the only way to wind a type down.

for a principal

Treat the directive as the org's only carve-out mechanism in a single schema, and be ready to say what turns a mark into a commitment — an owner, a replacement and a date — versus a schema that quietly fills with permanent tombstones.

## What kind of thing it is `@deprecated` is a **type-system directive**: it is written in the schema, next to a definition, not in an executable document a client sends. That distinction matters in interviews, because GraphQL's other well-known directives — `@include` and `@skip` — are executable directives that appear in documents and are evaluated per request. `@deprecated` is evaluated by nobody at request time. It is metadata the schema carries about itself. It is also **built in**: every spec-compliant server has it without anyone declaring it, and its definition is fixed: ```graphql directive @deprecated( reason: String = "No longer supported" ) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE ``` ## The four locations, and the edition that added two of them Originally the directive could only be attached to an **output field definition** and to an **enum value**. The October 2021 edition of the specification widened it to **argument definitions** and **input object field definitions**, and widened introspection to match. So on a modern server all four of these are legal: ```graphql enum StatementChannel { ONLINE POST FAX @deprecated(reason: "Withdrawn; use POST.") } input StatementFilter { accountId: ID! channel: StatementChannel paper: Boolean @deprecated(reason: "Use channel: POST.") } type Statement { id: ID! closingBalance: Money! balance: Money! @deprecated(reason: "Ambiguous mid-period; use closingBalance.") transactions(first: Int = 50, pageSize: Int @deprecated(reason: "Use first.")): [Transaction!]! } ``` Notice what is *not* in the list. You cannot deprecate an object type, an interface, a union, a scalar or an enum as a whole — the directive has no such location. To wind a type down you deprecate every field that returns it and wait for it to become unreachable. You also cannot deprecate a field of a type from outside; the mark lives on the definition. There is one validity rule worth memorising: **a required argument or required input field must not be deprecated**. "Required" here means a non-null type with no default value. The reasoning is mechanical — a caller cannot obey the deprecation, because omitting the argument makes the document invalid. If you want to retire a required argument, first give it a default or make it nullable, then deprecate it. ## What it does at request time: nothing This is the point candidates most often get wrong. Deprecation is **advisory**. A document that selects `balance` after `balance` is deprecated passes validation, executes, and returns the same value it always did. There is no warning in the `errors` array, no null, no status change. The specification defines no runtime consequence whatsoever. If your server surfaces deprecated-field usage somewhere, that is your instrumentation, not the protocol. That is exactly what makes the directive useful for versionless evolution: it lets you announce an intention without breaking anybody on the day you announce it. ## What it does do: change what introspection shows The visible behaviour is in the introspection system. Four introspection fields take an `includeDeprecated: Boolean = false` argument — `__Type.fields`, `__Type.enumValues`, `__Type.inputFields` and `__Field.args` — and by **default they omit deprecated members entirely**. Alongside that, `__Field`, `__EnumValue` and `__InputValue` each expose `isDeprecated: Boolean!` and `deprecationReason: String`. ```graphql query DeprecatedStatementMembers { __type(name: "Statement") { fields(includeDeprecated: true) { name isDeprecated deprecationReason } } } ``` The consequence is practical rather than dramatic. A deprecated field vanishes from the default listing an explorer, a documentation renderer or a typed client generator sees, so new code is unlikely to reach for it by accident, while everything already written keeps working. It also means "this field is missing from the docs" and "this field does not exist" are different situations, and the way to tell them apart is to introspect again with `includeDeprecated: true`. ## Writing a reason worth reading The `reason` argument defaults to the string `"No longer supported"`, which tells a reader nothing. A reason earns its place by answering the two questions the reader actually has: *what do I use instead*, and *how long have I got*. On a 37-field statements type where several members are on their way out, `@deprecated(reason: "Ambiguous mid-period; use closingBalance. Removal planned after 2026-11-30.")` is worth ten times a bare mark, because it is the only piece of the migration that travels all the way to the person reading generated types in an editor. What the directive cannot do is make the removal happen. Marking a field is a statement of intent; retiring it is a later, separately evidenced piece of work.

  • How would you deprecate an entire object type?
    You cannot, directly — @deprecated has no type-definition location, so an object type, interface, union, scalar or enum cannot be marked as a whole. What you do instead is deprecate every field that returns the type, so no client can reach it through a supported path, and remove the type once it is unreachable and no live document names it in a fragment condition.
  • Why does the specification forbid deprecating a required argument?
    Because the caller cannot comply. A required argument is non-null with no default, so a document that omits it fails validation — the deprecation would be asking clients to do something that is not allowed. The migration is two steps: first make the argument nullable or give it a default, which is itself a safe change, and only then mark it deprecated.
  • A client selects a deprecated field. What happens on the wire?
    Nothing different. Validation passes, the resolver runs, and the same value comes back in the same key. The specification defines no warning, no error entry and no header for deprecated-field usage. Anything you see about it — a log line, a metric, a lint failure in a client build — is instrumentation or tooling somebody added, not part of GraphQL.

saying these in an interview costs you the question

  • Says a deprecated field stops resolving or errors
  • Thinks @deprecated is written in the client's document
  • Claims deprecated fields disappear from the schema
  • Believes any type can be marked deprecated
  • Forgets introspection hides deprecated members by default
  • Leaves the reason at its default placeholder string

context