skip to content

Why does GraphQL introspection not show where a custom directive is applied?

level: seniorimportance: nice to knowfreq 20%

answer

  1. Definitions travel, applications do not
  2. The meta-types have no applications field
  3. Two built-ins got purpose-built holes
  4. Audit the SDL text, not the endpoint

basics

~20 s

Introspection describes the type system, so it lists a schema's directive definitions but never their applications. The only applications visible are two the meta-schema was given dedicated fields for: deprecated, as isDeprecated and deprecationReason, and specifiedBy, as specifiedByURL.

solid answer

~50 s

The `__schema` meta-field returns a `directives` list of full definitions - name, description, arguments, valid locations, an `isRepeatable` flag - so a tool can learn that a directive exists and what it accepts. What it cannot learn is which types, fields or enum values carry it: the meta-types `__Type`, `__Field`, `__InputValue` and `__EnumValue` have no applied-directives field, and there is no general API for one in the specification. Two applications leak through because the meta-schema carved out purpose-built holes for them: `@deprecated` appears as `isDeprecated` and `deprecationReason`, and `@specifiedBy` as `specifiedByURL` on a custom scalar. The practical consequence is that anything consuming the schema through introspection alone - a typed client generator, a linter pointed at a deployed endpoint, an explorer, a homegrown audit script - is blind to your annotations. If a tool must see them, give it the SDL text, from version control or a schema registry.

code

graphql · 8 lines
graphql
{
  __schema {
    directives { name isRepeatable locations args { name } }
  }
  __type(name: "SoilProbe") {
    fields { name isDeprecated deprecationReason }
  }
}

go deeper

for a junior

Know that introspection tells you a schema's types and fields, and that it lists which directives exist without telling you where each has been used. Nobody will hold this against you at this level.

for a middle

Be able to distinguish a directive definition from a directive application and say which of the two introspection returns. Naming the deprecation fields as the visible exception is a good sign you have read a real introspection response.

for a senior

Show you would reach for the SDL rather than the endpoint when you need to audit annotations, and that you know a printed schema may have dropped applications during schema construction. This is exactly where a homegrown coverage report quietly reports a false clean bill of health.

for a principal

Own the consequence for a platform: a directive-driven convention is invisible to every standard tool, so the schema text has to become a governed artefact - registered, diffed and checked in CI - or the convention will drift silently as the graph grows.

Introspection is a GraphQL server's self-description, and it is deliberately a description of the **type system**, not of the SDL text. The distinction matters most for directives, because introspection is generous about directive *definitions* and almost silent about directive *applications*. ## What introspection actually returns The `__schema` meta-field exposes a `directives` list of `__Directive` values, and each one carries the directive's name, description, argument definitions, the locations it is valid at, and an `isRepeatable` flag: ```graphql { __schema { directives { name isRepeatable locations args { name } } } } ``` That is the complete definition. From it a tool can learn that a directive called `herdScoped` exists, that it takes a `herdIdArg` argument, and that it is valid on `FIELD_DEFINITION`. What it cannot learn is **which fields actually carry it**. The meta-types describing the schema's contents — `__Type`, `__Field`, `__InputValue`, `__EnumValue` — have no field listing the directives applied to them. There is no `appliedDirectives`, and none of the meta-types has one. ## The two exceptions, and why they are exceptions Two directive applications are visible, and only because the meta-schema was given dedicated fields for them rather than a general mechanism: - `@deprecated` surfaces as `isDeprecated: Boolean!` and `deprecationReason: String` on `__Field` and `__EnumValue` (and, in later editions, on `__InputValue` for deprecated arguments and input fields). - `@specifiedBy` surfaces as `specifiedByURL: String` on `__Type` for custom scalars. These are purpose-built holes, not an API. They exist because tooling had a concrete, universal need — mark this field struck through in an explorer, link this scalar to its human-readable specification — and the meta-schema answered that need directly. Adding a general applications API has been discussed in the community for years; it is not in the specification, and you should say so rather than assume it. ## Why it is designed that way Introspection describes the schema a server *executes*, not the file a human wrote. By the time a request arrives, a custom directive has usually already done whatever it was going to do — the server read it while assembling the executable schema and may have wrapped a resolver, registered a policy, or simply recorded a note. The application is an input to schema construction, and once construction is finished it is not part of the type system any more unless the server chose to keep it. Exposing it would also mean exposing internal policy annotations to anyone who can reach the endpoint, which is not obviously desirable. ## What this costs in practice Anything that consumes the schema **only through introspection** is blind to your annotations. That includes a typed client generator, a schema linter run against a deployed endpoint, an explorer, and any homegrown audit script that walks `__schema`. All of them will show a schema that looks entirely ordinary. A farm sensor graph makes the failure concrete. The team defined a custom `@herdScoped` directive and a server-side wrapper that reads it at schema build time to gate a field on the caller's herd. To prove coverage they wrote an audit that introspected the deployed endpoint and listed every field, expecting to see which ones were annotated. The audit could only read `isDeprecated`, so it reported nothing at all about scoping — and the team read "no unscoped fields reported" as "no unscoped fields". Two probe fields added in a later release had never been annotated. The gate that should have run before those fields resolved ran only where the annotation existed, so for those two fields the check effectively arrived after the data had already been produced. Nothing was misconfigured; the audit was asking a question introspection cannot answer. (How to design that gate is a separate subject; the point here is that introspection was the wrong instrument for verifying it.) ## What to do instead Treat the **SDL** as the artefact that carries applications, and introspection as the artefact that carries the type system. Concretely: - Check the SDL into version control and audit that text, not the endpoint. - Publish the SDL to a schema registry and make the registry — not introspection — the source for any tool that must see annotations. - Where a server can print SDL back out of the executable schema, verify it actually retains applied directives; some construction paths drop them, and a printed schema that has lost them is as blind as introspection. - If a client genuinely must learn an annotation at runtime, expose it as ordinary schema — a real field or a documented extension — rather than hoping introspection will leak it. ## The one-line answer Introspection returns directive **definitions**, never their **applications**, with two hard-coded exceptions the meta-schema carved out for `@deprecated` and `@specifiedBy`. If a tool needs to know where a directive was applied, give it the SDL.

  • How would you make a directive application visible to a tool that needs it?
    Give the tool the SDL rather than the endpoint: check the schema text into version control, or publish it to a schema registry and let tooling read it from there. Where a server can print SDL back out of the executable schema, verify it actually retains applied directives first - some construction paths drop them, and a printed schema that has lost them is as blind as introspection.
  • Is `__schema { directives }` useful for anything, then?
    Yes, and it is there for a specific reason: it carries each directive's name, arguments, valid locations and repeatability, which is exactly what a client or an editor needs to write and validate an operation document that uses an executable directive. It answers "may I write this here, and what does it take", not "where has it been written".
  • Why did the specification expose `isDeprecated` rather than a general applications list?
    Those fields answer a concrete, universal tooling need - strike this field through in an explorer, link this scalar to its written specification - and the meta-schema served that need directly rather than by generalising. A general applied-directives API has been discussed in the community for years but is not in the specification, and it is worth saying so rather than assuming it exists.

Introspection hands you a library's catalogue of every kind of sticky note the staff may use, but not a list of which books have one stuck on the cover. For that you have to walk the shelves - or read the original manifest.

saying these in an interview costs you the question

  • Thinks introspection lists every directive applied to a field
  • Expects a client generator to see a custom directive
  • Treats isDeprecated as a general applied-directive API
  • Believes disabling introspection is what hides applications
  • Concludes the server must be misconfigured
  • Invents an appliedDirectives field on the meta-types

context