Skip to content
intermediate Phase 12 · Developer Tooling: Introspection, GraphiQL, and Codegen

Introspection Query Mechanics

The __schema and __type queries that power tooling and documentation sites.

45m
1 problems
Topic Progress 0%

Every GraphQL Tool Is Just an Introspection Client

Every GraphQL Tool Is Just an Introspection Client

A linter, a code generator, and a documentation site all need the same thing: your schema as data. They do not read your repository, they do not share your build, and they have no special protocol. They all send an ordinary GraphQL query asking the server to describe itself.

Two meta-fields on the query root provide the entire mechanism:

{ __schema { queryType { name } } }          # the whole schema
{ __type(name: "Post") { kind name } }       # one type by name

Both are reserved by the specification — every conforming server implements them, and you cannot rename or remove them. The types they return are also meta-types: __Schema, __Type, __Field, __InputValue, __EnumValue, __Directive, and the __TypeKind enum. Together they can describe any schema: objects with their fields and arguments, interfaces with their implementors, unions with their possible types, enums with their values, inputs with their defaults, and directives with their locations.

That self-description is why GraphQL clients need no IDL file to interoperate. A mobile client, a docs generator, and a mock server can all start from one URL. It is also why the tools you already know behave the way they do: GraphiQL's docs panel fires __schema at load (see the GraphiQL workbench topic), a codegen run either queries __schema or reads an SDL file that someone previously produced from it, and a gateway builds its router from the same document.

The consumer list is worth being concrete about:

Consumer What it extracts from introspection
GraphiQL / IDE plugins fields, args, enum values for completion
graphql-codegen type definitions and operation result shapes
Schema linters AST to run naming and description rules against
Docs sites types with descriptions and deprecation reasons
Mock servers field types to synthesize fixture data
Gateways / registries composition and routing metadata

One boundary matters before going further: introspection is a read API over your schema, and like any read API it can be gated. Servers expose a switch to disable it for untrusted callers — when that switch is off, every consumer above must be given a schema artifact instead. Schema exposure control covers the policy; the rest of this topic covers the mechanics and the artifact path.

Reading the __schema Document

Reading the __schema Document

Here is a single introspection query that returns everything most tools need:

{
  __schema {
    queryType { name }
    mutationType { name }
    types {
      kind
      name
      description
      fields(includeDeprecated: true) {
        name
        description
        args { name type { ...TypeRef } defaultValue }
        type { ...TypeRef }
        isDeprecated
        deprecationReason
      }
      inputFields { name type { ...TypeRef } defaultValue }
      interfaces { kind name }
      enumValues(includeDeprecated: true) {
        name
        isDeprecated
        deprecationReason
      }
      possibleTypes { kind name }
    }
    directives {
      name
      locations
      args { name type { ...TypeRef } defaultValue }
    }
  }
}

fragment TypeRef on __Type {
  kind
  name
  ofType { kind name ofType { kind name ofType { kind name ofType { kind name } } } }
}

Three details decide whether the result is usable:

Wrapper types are chains, not names. A nullable list of non-null strings [String!]! arrives as { kind: "NON_NULL", ofType: { kind: "LIST", ofType: { kind: "NON_NULL", ofType: { kind: "SCALAR", name: "String" } } } }. name is null for wrapper kinds and set only at the leaf, which is what the nested ofType selection in TypeRef is for. graphql-js's own getIntrospectionQuery() nests seven levels deep because schemas occasionally wrap that far.

Deprecated members are hidden by default. fields and enumValues take includeDeprecated with a default of false, so a tool that forgets the flag silently loses every deprecated field — and a schema diff built from that output would report false removals. Passing true also returns deprecationReason, which docs and codegen need.

__type(name:) drills in. When a tool needs one type — completion for a single object, a lint pass on one file — __type avoids shipping the whole schema document. kind tells you which sibling fields are meaningful: OBJECT has fields, INPUT_OBJECT has inputFields, UNION and INTERFACE have possibleTypes (for unions) and interfaces, ENUM has enumValues.

Once you have the response, the round trip back to SDL is three calls in graphql-js:

import { getIntrospectionQuery, buildClientSchema, printSchema } from 'graphql';

const res = await fetch(ENDPOINT, {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ query: getIntrospectionQuery() }),
});
const { data } = await res.json();
const sdl = printSchema(buildClientSchema(data));  // introspection JSON -> SDL string

getIntrospectionQuery() returns the same document you would hand-write (with directive and isRepeatable detail), buildClientSchema turns the result into an in-memory schema object, and printSchema renders it as the SDL you commit, lint, and diff.

Snapshots, Artifacts, and Locked-Down Endpoints

Snapshots, Artifacts, and Locked-Down Endpoints

Production endpoints increasingly answer introspection requests with a refusal — the exposure risk outweighs the convenience for public APIs. Tooling does not stop; it switches inputs: instead of querying a live server, it reads an artifact produced at release time.

A minimal export step, run wherever the schema is authoritative (usually the repo, not production):

# schema.json for tools that speak introspection, schema.graphql for humans and linters
graphql-codegen --config introspect.ts
# or directly, with no codegen involved:
node -e "const {getIntrospectionQuery}=require('graphql');process.stdout.write(getIntrospectionQuery())" > /tmp/introspect.graphql

Committing the artifact buys four things:

  1. Air-gapped and offline tooling. Linters, codegen, and mock generation run without network access or credentials.
  2. Deterministic CI. A diff between last release's schema.json and this branch's is a breaking-change check that needs no running server. That comparison is the heart of schema checks in CI.
  3. No prod exposure. The live endpoint can keep introspection disabled while consumers still get a complete, versioned schema description from the registry or the repo.
  4. Reproducible builds. A client build from six months ago can regenerate identical types because its schema input is pinned in the repository.

Artifacts introduce one maintenance problem — staleness — and the fix is mechanical: regenerate in CI and fail if the committed file differs from the freshly generated one, the same dry-run idea as graphql-codegen --check. The schema-changing pull request updates artifact and code in one review, so the artifact can never drift further than one merge.

Practical notes: introspection results are large (hundreds of kilobytes for a real schema) because of the nested ofType chains and repeated metadata — either commit the printed SDL (smaller and diff-friendly) and reconstruct introspection when a tool insists, or commit minified JSON and accept the noise in reviews. Whichever you pick, pick one canonical artifact per schema and make every consumer read it; two artifacts that disagree are two schemas.

Interview framing: expect "how do you get the schema into your codegen if introspection is off in production?" The answer has three parts: export introspection (or print SDL) at the point where the schema is built, commit it as an artifact, and keep it honest with a regeneration check in CI.

Practice Problems

0 / 1 solved
Write an Introspection Query for Tooling
Introspection authoring

Write one introspection query that returns every field of the Query root type with its argument names, full argument types (including wrapper chains), and deprecation status. Then state how CI converts the response into SDL when production introspection is disabled.

Example:

Input: type Query { post(id: ID!): Post! oldSearch(q: String): [Post] @deprecated(reason: "use posts") }

Output: A __type(name: "Query") selection returning fields[ { name, type: TypeRef, args: [{ name, type: TypeRef }], isDeprecated, deprecationReason } ] with includeDeprecated: true

Query is an object type, so __type with fields(includeDeprecated: true) gives the root field list; args carry their own TypeRef chains so ID! and [Post] survive the round trip.

Optimal Solution — O(s) time, O(s) space

Drill into a single type with __type(name:) rather than walking the whole __schema document, select fields with includeDeprecated: true, and reuse a TypeRef fragment so every argument and field type keeps its wrapper chain. For CI, export the same document once at the authoritative source, commit the response, and regenerate on every schema pull request.

query IntrospectQueryRoot {
  __type(name: "Query") {
    kind
    name
    fields(includeDeprecated: true) {
      name
      description
      isDeprecated
      deprecationReason
      type { ...TypeRef }
      args {
        name
        defaultValue
        type { ...TypeRef }
      }
    }
  }
}

fragment TypeRef on __Type {
  kind
  name
  ofType { kind name ofType { kind name ofType { kind name ofType { kind name } } } }
}

// ---- CI conversion (schema artifact path) ----
import { getIntrospectionQuery, buildClientSchema, printSchema } from 'graphql';

const sdl = printSchema(
  buildClientSchema(await fetchIntrospection(process.env.SCHEMA_ARTIFACT_URL)),
);
await writeFile('schema/schema.graphql', sdl);   // committed; diffs gate every PR

Edge Cases:

  • A root field with zero arguments (args: [])
  • Deeply nested wrappers beyond four levels (raise the ofType depth or use getIntrospectionQuery)
  • Schema where the query root type is renamed (look it up via __schema.queryType, not the name Query)

Quiz

1. A tool queries a field list without arguments and notices deprecated fields are missing from the result. What was omitted?

Question 1 options

2. How is the type String! encoded in an introspection result?

Question 2 options

3. What is the difference between __schema and __type(name: "Post")?

Question 3 options

4. Production has introspection disabled. How does a client codegen build get its schema?

Question 4 options

Flashcards

Question

The two introspection entry points

Answer

__schema for the whole schema, __type(name:) for a single type — both reserved root fields.

Question

Meta-type set

Answer

__Schema, __Type, __Field, __InputValue, __EnumValue, __Directive, plus __TypeKind and __DirectiveLocation enums.

Question

How [String!]! appears in JSON

Answer

NON_NULL -> LIST -> NON_NULL -> SCALAR String, each wrapper a separate node linked by ofType; name only at the leaf.

Question

includeDeprecated default

Answer

false — deprecated fields and enum values are omitted unless you pass true, and only then do you get deprecationReason.

Question

Introspection-to-SDL trio in graphql-js

Answer

getIntrospectionQuery() to fetch, buildClientSchema() to parse, printSchema() to render SDL.

Question

Schema artifact workflow

Answer

Export at the authoritative source, commit it, regenerate in CI and fail on drift — lets prod keep introspection off.

Revision Notes

Key Takeaways

  • 1. Every GraphQL tool is an introspection client: one query, no special protocol
  • 2. Wrapper types are ofType chains; names exist only at leaves
  • 3. Deprecated members are hidden unless includeDeprecated: true
  • 4. Artifacts replace live introspection in CI and are kept honest by a regeneration check

Interview Tips

  • • Be able to write the introspection query from memory: __schema, types, fields with args, TypeRef nesting
  • • Explain the ofType chain with the concrete example of [String!]! — interviewers use it to check you have read a real result
  • • Have an answer ready for "introspection is off in production; how does codegen work?"

Cheat Sheet

Introspection Cheat Sheet

{
  __schema {
    queryType { name }
    types {
      kind name description
      fields(includeDeprecated: true) {
        name args { name type { ...TypeRef } defaultValue }
        type { ...TypeRef } isDeprecated deprecationReason
      }
      inputFields { name type { ...TypeRef } }
      enumValues(includeDeprecated: true) { name isDeprecated deprecationReason }
      interfaces { name } possibleTypes { name }
    }
    directives { name locations args { name type { ...TypeRef } } }
  }
}
fragment TypeRef on __Type {
  kind name ofType { kind name ofType { kind name ofType { kind name } } }
}

Rules

  • wrappers are ofType chains; name is null on NON_NULL/LIST
  • fields / enumValues: includeDeprecated defaults to false
  • __type(name: "Post") for one type; __schema for everything

Round trip

getIntrospectionQuery() -> buildClientSchema() -> printSchema()  // SDL for commit/CI

Prod with introspection off: commit schema.json/schema.graphql, regenerate in CI, fail on drift.

Continue Learning