GraphQL Directive

Pankaj Kumar
9 min readSep 29, 2022

A GraphQL directive is one of the most powerful tools to customize and add new functionality to the GraphQL API. It can support many use cases such as access control, input validation, caching, etc.

Like queries and mutation, a GraphQL directive is defined in GraphQL Schema Definition Language (SDL) and it can be used to enhance the behavior of either schema or operation.

In this article, we’ll understand GraphQL directives with code examples in Spring for GraphQL.

Let’s get started!

Want to know more about GraphQL? You can check my other posts:

Code Example

The working code example of this article is listed on GitHub. To run the example, clone the repository, and import graphql-spring-directive as a project in your favorite IDE as a Gradle project.

You can find more information in README.md of the repository.

What are Directives in GraphQL?

A GraphQL directive is a way to add an extra behavior by annotating parts of the GraphQL schema. We can define a directive starting with @ character followed by the name, argument (optional), and execution location.

GraphQL specification defines directive as

DirectiveDefinition:

Descriptionopt directive @ Name ArgumentsDefinitionopt on DirectiveLocations

For example, a built-in directive @deprecated is defined as:

directive @deprecated( 
reason: String = "No longer supported"
) on FIELD_DEFINITION | ENUM_VALUE

Here,

  • The name of the directive is @deprecated.
  • The @deprecated directive takes an optional argument reason with the default value "No longer supported".
  • And, this directive can be applied to a field definition and enum values.

As the name suggests, using the @deprecated directive we can mark a certain field of the API as deprecated.

For example, if you decide to introduce a more descriptive field called bookName in place of the old name field then you can use @deprecated directive to mark name as deprecated as:

GraphQL Directive Type

Based on where directives are applied, we can categorize directives as Schema and Operation directives.

Schema Directive

A schema directive is applied to the schema of GraphQL (specified as TypeSystemDirectiveLocation in GraphQL specification). One example of a schema directive is @deprectaed, which allows us to annotate an API field as deprecated.

A schema directive can be applied to one of the following.

SCHEMA 
SCALAR
OBJECT
FIELD_DEFINITION
ARGUMENT_DEFINITION
INTERFACE
UNION
ENUM
ENUM_VALUE
INPUT_OBJECT
INPUT_FIELD_DEFINITION

Operation Directive

An operation directive is applied to the operations (query and mutation) and therefore affects how an operation is processed by the GraphQL server. An example of a built-in operation directive is @skip, defined as:

directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT on FIELD_DEFINITION

An operation directive can be applied to one of the following.

QUERY 
MUTATION
SUBSCRIPTION
FIELD
FRAGMENT_DEFINITION
FRAGMENT_SPREAD
INLINE_FRAGMENT

GraphQL built-in Directives

At the time of writing this article, GraphQL spec talks about three built-in directives — @skip, @include, and @deprecated.

@skip

You can use @skip directive on fields, fragment spreads, and inline fragments. This directive allows for conditional exclusion during execution based on the 'if' argument.

directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT

For example, to conditionally exclude the comment field of ratings for operation GetBooks, you can use @skip as:

Where $itTest is a GraphQL variable.

Variables need to be defined as arguments of the operation name before you can refer to them in the query.

In GraphiQL, you can set variable values in the variable section as shown below.

@include

Similar to @skip, @include is an operation directive defined as :

directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT

You can use @include directive on fields, fragment spreads, and inline fragments and it allows for conditional inclusion during execution based on the 'if' argument.

@deprecated

The @deprecated directive is a schema directive and it can be used to mark a field or enum value as deprecated.

directive @deprecated( reason: String = "No longer supported" ) on FIELD_DEFINITION | ENUM_VALUE

Directive Use Cases

The power of directive comes from the fact that the GraphQL specification doesn’t mandate any restrictions on its usage. Therefore, you are free to define your own directives to support as many use cases as you want.

Let’s look at a couple of use cases of GraphQL directives along with implementation.

Use case 1: Access Control

One of the most common use cases for any public API is to support access control and GraphQL-based public API is no different.

Let’s try to understand, how we can implement access control using directives.

Imagine the type Book has a sensitive field called revenue.

And, you want to show revenue information only to a user with a manager role.

To handle this use case, you can define a schema directive called @auth and apply this directive to the field revenue as:

When any user queries the revenue field, the service implementation can check if the user has the required role before returning a response back to the user. The role check can be done using claims of user tokens or some other mechanism. For simplicity, we can just assume that role is passed in the HTTP header in the API request.

Running in GraphiQL, with the role passed as an HTTP header:

Query without manager role:

Implementing @auth Directive

To implement any schema directive in Spring for GraphQL, we need to modify the original data fetcher by creating a custom data fetcher as a decorator. The role of the custom data fetcher is to do the authorization check before delegating calls to the original data fetcher.

For that, let’s first define a class AuthDirective that extends SchemaDirectiveWiring and override the onField (as schema directive method @auth is applied on the field) as:

Then, we define a new data fetcher authDataFetcher as:

In the above code,

  1. The authDataFetcher first checks if the user has the required role by getting the role from the GraphQlContext as graphQlContext.get("role").
  2. If the user has the required role then it calls the original data fetcher.
  3. If the user doesn’t have the required role then it returns null. Thus, an unauthorized user sees a null value in the API response.

And the last step is to set authDataFetcher as a new data fetcher for the field revenue and parent type book.

In the above code, we have resolved role information from the graphQlContext as graphQlContext.get("role"). We can use WebGraphQlInterceptor to set the role in the GraphQLContext as:

Complete code for AuthDirective:

Also, we need to make spring aware of the schema directives:

On application startup, GraphQL java engine calls AuthDirective onField method and assigns authDataFetcher as new Data Fetcher for the fields marked with directive @auth.

Use case 2: Input Validation

Another common use case supported by any API is to validate the user’s input and return helpful error messages. Let’s understand how GraphQL directives can be used to implement this feature.

Consider input type BookInput

What if you have business rules that say that the name, author, and publisher fields can be of minimum size 10 and max size 100?

One approach for implementing such business validation is to define validation in the BookInput object (mostly using javax.validation) as:

The problem with the above approach is that a client will only discover such validation at runtime after it has made the request. A better approach is to define business validations in SDL, using directives, so that it becomes part of API documentation (and also discoverable by clients using introspection).

Implementing Input Validation Directive

You can implement input validation using graphql-java-extended-validation. For example, to validate the size of input fields we can use graphql-java-extended-validation directive @size on SDL (Schema Definition Language) as:

For this, first, add the dependency on graphql-java-extended-validation in build.gradle as

implementation 'com.graphql-java:graphql-java-extended-validation:18.1-hibernate-validator-6.2.0.Final'

and then create ValidationRules and wire that with RuntimeWiring.Builder as :

Running in GraphiQL

Use case 3: Adding Functionality to the Query

Let’s try to understand how directives can be used to enhance the behavior of operation (queries and mutation) with an example.

Imagine the book catalog API always returns the price in default currency $.

What if a client wants to show the price in a different currency? How can you build an API allowing clients to dynamically request prices in other currencies?

To solve such use cases, you can define an operation directive @currency that takes the target currency as an argument.

Then, the client can call this API by passing currency as an argument as:

Notice that the field price @currency(currency: "INR") returns the price in the requested currency INR.

One obvious problem with the operation directive is that there is no stopping client from applying the @currency directive on fields other than the price field.

Implementing Operation Directive

Compared to schema directives, operation directives are complicated to implement as they are supplied by the client with instructions to change the behavior of the operation.

Therefore, the only option we have is to use Data Fetcher call back and add custom behavior based on the directive.

In the default implementation, when the directive @currency is supplied to the field price, the GraphQL engine simply ignores this directive and returns the price based on the default implementation graphql.schema.PropertyDataFetcher(as discussed in the earlier article).

We can override this behavior by defining a Data Fetcher for the price as

And then add additional behavior based on the directive supplied by the client as:

In the above code,

  1. We read the applied directive as dataFetchingEnvironment.getQueryDirectives().getImmediateAppliedDirective(“currency”);
  2. Then read the argument of the applied directive as maybeAppliedDirective.get(0).getArgument("currency").getValue(). Here we have made the assumption then there is only one directive is applied to the field price.
  3. Then call CurrencyConversionService by passing target currency information.

Rather than hardcoding default value in code, you can also define the default value of the currency in the directive definition as:

directive @currency( currency: String! = "$" ) on FIELD

and read the default value in the code.

Directive Documentation

Documentation is the first‐class feature of GraphQL and all GraphQL types, fields, arguments and other definitions should provide a description unless they are considered self-descriptive.

We can document directives as:

Then, a client can also fetch all directives supported from the server as:

In GraphiQL

Summary

  • A GraphQL directive is a way to add an extra behavior by annotating parts of the GraphQL schema. We can define a directive starting with @ character followed by the name, argument (optional), and execution location.
  • Based on where directives are applied, it can be used to either customize the behavior of operation (mutation or queries — known as Operation directive) or schema (known as Schema directive).
  • There can be many use cases of directives — such as authorization, input validation, caching, etc.

Originally published at https://techdozo.dev on September 29, 2022.

--

--

Pankaj Kumar

Software Architect @ Schlumberger ``` Cloud | Microservices | Programming | Kubernetes | Architecture | Machine Learning | Java | Python ```