GraphQL deprecation without a version number
5 min read
A REST API can ship /v2/ alongside /v1/ and let callers move at their own pace. GraphQL has
one schema at one endpoint, and every client, the mobile app on last year’s build and the internal
dashboard deployed this morning, queries the same graph. There is no URL to fork. Deprecating a
field means marking it deprecated in place, in a schema everyone already depends on, which makes
the discipline different from REST even though the underlying problem, telling callers something
is going away, is the same one API deprecation covers in general.
How does GraphQL mark a field deprecated, if there’s no version to bump?
With the @deprecated directive, applied to the field itself:
type Product {
price: Float @deprecated(reason: "Use priceV2 for multi-currency support.")
priceV2: Money
}
The field stays queryable. It does not disappear, does not 404, does not change behavior; it just carries a machine-readable note that most GraphQL tooling, GraphiQL, Apollo Studio, schema linters, will surface to anyone browsing the schema or writing a query against it. This is the entire mechanism. There is no separate deprecation endpoint, no header, no companion document required by the spec, which is both the appeal and the trap: the directive is easy to add and easy to ignore, because nothing forces a client to look at it.
Does anyone actually see the deprecation reason?
Only people using the schema directly, through introspection or a schema-aware editor, and that is
a smaller audience than an API changelog’s usual readers. A mobile app built against a query six
months ago has already baked that query into its binary; it will keep asking for price and
keep getting an answer, deprecated or not, until someone rebuilds the app with the new field and
ships an update. The directive tells a developer writing new code not to use the old field. It
does nothing for the client already shipped and running.
| Mechanism | Who it reaches |
|---|---|
@deprecated directive | Developers browsing the schema or writing new queries |
| Schema-linter CI failures | The team that owns the client codebase, if they run one |
| A changelog entry | Anyone who reads it, including a client team with no linter |
| Nothing (field just works) | An already-built client using the old field |
Should a deprecated field still get a changelog entry?
Yes, and it is doing more work than the directive alone, because a changelog reaches people the directive cannot: a partner team that consumes the graph without browsing its schema, a client built against a cached copy of the schema from months ago, anyone who would only notice by reading prose. API changelog covers what an entry owes a caller in general; a GraphQL entry owes one thing REST rarely has to spell out, because REST callers infer it from the version number: whether the old field still works today, still works with a warning, or has actually stopped returning data. The directive alone answers none of that for a reader who has not opened the schema.
When is a field actually safe to remove from the schema?
Only once query logs show nothing is asking for it, which is a usage question, not a calendar
question. A field can carry @deprecated for a year and still be load-bearing for one client that
never rebuilt; removing it on a fixed timeline, the way a REST Sunset header often does, breaks
that client with no warning it can act on, because GraphQL gives it nothing to act on beyond the
directive it never read. Log field-level usage before committing to a removal date, and treat a
non-zero query count as a hold, not a countdown.
Does adding a field carry the same risk as in a REST API?
Less, for a new field, because a GraphQL client only receives the fields it explicitly asks for.
Adding priceV2 next to price cannot break an existing query the way adding a field to a REST
JSON response can break a strict deserializer, since nothing forces the client to request the new
field. Adding a value to an existing enum is the exception worth naming in the same breath: a client
that switches on every enum value exhaustively, which strongly typed languages encourage, breaks the
moment a new value arrives, whether or not any query asked for it. The safety only holds for fields
and union members a client opts into; it does not hold for a closed set a client’s code enumerates
by hand.
What does a GraphQL changelog entry need that a REST one doesn’t?
The query shape, not just the field name, because “the price field is deprecated” is missing
the piece a caller actually needs: which types and which queries touch it. A useful entry names
the type, the field, the replacement field, and, if you can generate it, the actual queries in
production still requesting the old shape. That last piece, tying the deprecation notice to real
usage, is the thing REST callers get for free from server logs on a URL and GraphQL callers do
not, because every query hits the same endpoint regardless of what it asks for.
Can anything other than a field carry the @deprecated directive?
Enum values, using the same directive at the value’s own definition rather than the field’s:
enum ShippingMethod {
STANDARD
EXPRESS
OVERNIGHT @deprecated(reason: "Use EXPRESS with priority: true instead.")
}
The spec defines @deprecated for exactly two locations, a field definition or an enum value, and
nothing else as of the stable release; argument- and input-field-level deprecation exist only in
later draft language, not in what most servers implement today. An enum value marked this way stays
a legal value a server can still return or accept, the same non-breaking promise a deprecated field
makes, which is what makes it safe to ship ahead of removing the value for real.
FAQ
Does GraphQL support anything like a Sunset header for a whole endpoint?
No, because there is usually only one endpoint. Deprecation timing lives at the field level, in
the @deprecated directive’s reason text and in whatever changelog or migration guide a team
publishes alongside it, not in a response header a client can read programmatically.
Can a deprecated field be removed and re-added later with a different type?
Only as a new field name. Reintroducing the same field name with a changed type is exactly the
breaking change the deprecation cycle exists to avoid; give the replacement its own name, the way
priceV2 does, and let the old one age out completely before the name is free to reuse.
Should the @deprecated reason text link to the changelog entry?
Yes, when the schema tooling supports it. The reason field accepts a plain string, and a URL
inside that string is the shortest path from a developer staring at introspection output to the
fuller explanation a changelog entry can give.
Is a GraphQL schema change ever backward compatible in a way REST isn’t? Additive field changes, yes, for the reason above: clients only get what they ask for. New enum values are the exception, because a client enumerating a closed set can break on one it did not expect. Removals and type changes are exactly as breaking as their REST equivalents.
The technical claims in this article have not been independently reviewed. If something here is wrong, tell us and we will correct it.