GraphQL’s built-in directives: finer control over queries and schemas

Directives are among the most useful—yet least discussed—features of GraphQL. Every API that conforms to the GraphQL specification must implement a set of built-in schema and operation directives, and they give client developers a way to shape responses dynamically without maintaining multiple query variants. For applications with configurable UIs—say, a data table where users can show or hide columns—this means you can avoid fetching data that will never be rendered.

The GraphQL specification defines directives according to where they can be applied. Consumer operations such as queries can use operation directives, which execute when the query runs. Schema directives, by contrast, are applied when the schema itself is defined. In practice, directives serve a range of purposes: adding metadata, providing runtime hints, controlling runtime parsing (such as returning dates in a particular format), and offering extended descriptions like deprecation notices.

The four core directives

The specification’s working draft presently lists four main directives:

  • @include
  • @skip
  • @deprecated
  • @specifiedBy (working draft)

Two further directives—@stream and @defer—have been merged into the JavaScript implementation and can be tried today, but they are not yet part of the official spec while the community evaluates them in real-world use.

Conditional inclusion with @include

The @include directive conditionally includes a field when its if argument evaluates to true. Because the condition is often dynamic, it’s natural to pass a variable into the query to determine truthiness:

query getUsers($showName: Boolean) {
  users {
    id
    name @include(if: $showName)
  }
}

When the variable $showName is false, the name field is omitted from the response. You can also provide a default for the variable so that the query works without the client passing it each time:

query getUsers($showName: Boolean = true) {
  users {
    id
    name @include(if: $showName)
  }
}

Skipping fields and fragments

The @skip directive expresses the inverse behavior: when its if argument is true, the field is left out of the response.

query getUsers($hideName: Boolean) {
  users {
    id
    name @skip(if: $hideName)
  }
}

Applying these directives one field at a time gets repetitive when you want to toggle several related fields. You might be tempted to duplicate the directive across multiple lines:

query getUsers($includeFields: Boolean) {
  users {
    id
    name @include(if: $includeFields)
    email @include(if: $includeFields)
    role @include(if: $includeFields)
  }
}

A tidier approach is to exploit the fact that both @skip and @include can be placed on fragment spreads and inline fragments, not just on individual fields. That lets you group fields behind a single conditional using inline fragments:

query getUsers($excludeFields: Boolean) {
  users {
    id
    ... on User @skip(if: $excludeFields) {
      name
      email
      role
    }
  }
}

If you already have a named fragment, the same directives can be applied at the point where the fragment is spread into the query:

fragment User on User {
  name
  email
  role
}

query getUsers($excludeFields: Boolean) {
  users {
    id
    ...User @skip(if: $excludeFields)
  }
}

Marking schema fields as @deprecated

Unlike the conditional directives, @deprecated is not something a client sends in a query. It appears in the schema, where the API maintainer marks a field as no longer recommended. When a client requests a deprecated field, the schema can return a warning with contextual guidance.

A zoomed in example of a syntax-highlighted GraphQL query for getUsers, which contains a users object with id and title properties. The title property is underlined in yellow with a contextual tooltip open below it showing a warning in yellow and white that suggests using the name field instead.
In this example, the title field has been marked deprecated and the directive provides a helpful hint to replace it.

To deprecate a field, add the @deprecated directive in the schema definition language (SDL) and pass a reason argument:

type User {
  id: ID!
  title: String @deprecated(reason: "Use name instead")
  name: String!
  email: String!
  role: Role
}

Because @deprecated is schema-level and @include is operation-level, the two can be combined: a client can choose to fetch a deprecated field only when a particular variable is set.

fragment User on User {
  title @include(if: $includeDeprecatedFields)
  name
  email
  role
}

query getUsers($includeDeprecatedFields: Boolean! = false) {
  users {
    id
    ...User
  }
}

Documenting custom scalars with @specifiedBy

The fourth built-in directive, @specifiedBy, is still in the working draft. It targets custom scalar implementations and takes a single url argument pointing to the specification for that scalar. For instance, if your API defines a custom EmailAddress scalar based on a regular expression, the schema entry can reference the relevant specification (such as RFC #822 for email format):

scalar EmailAddress @specifiedBy(url: "https://www.w3.org/Protocols/rfc822/")

When creating your own custom directives, it’s advisable to prefix the name in order to prevent collisions with other directives. A useful example of a real-world custom directive is the GraphQL Public Schema project, which supports both code-first and schema-first approaches for annotating which parts of an API may be consumed publicly.

Why directives matter

The built-in directives are easy to overlook, especially next to more celebrated GraphQL features like the type system and introspection. Yet they provide a meaningful layer of control: schema authors can guide consumers with deprecation warnings and scalar documentation, while clients can fine-tune payloads field by field. Including these directives in your introspection query also improves developer experience—knowing a field is deprecated, and why, without ever leaving the code editor is a small but powerful convenience.