API changes

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.

MechanismWho it reaches
@deprecated directiveDevelopers browsing the schema or writing new queries
Schema-linter CI failuresThe team that owns the client codebase, if they run one
A changelog entryAnyone 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.

Related on changeloop: Developer docs, Changelog tools compared

changeloop
The team building a closed-loop changelog. Your users ask, your team ships, the person who asked gets told.