Within this article we will explore how to implement custom GraphQL directives to protect content from exposure through your API.
GraphQL (https://graphql.org/) is a modern way to conceive the interaction between browsers and web applications. Owing to its features, being strongly typed and yet so flexible, with built-in schema introspection capabilities, it’s a really good choice for implementing a replacement to conventional RESTful approach, or even for flanking a pre-existing API with the target of widening the API offer of your application server.
We’re not giving an introductory tutorial on GraphQL here, though; we assume a basic knowledge of GraphQL, as well as a solid knowledge of Java Spring Boot.
Anyways, even if you’re new to these concepts, we’ll try to keep things simple as much as possible, so to let you enjoy the wonders of GraphQL.
Also, make sure to check out Andrea Gazzarini’s outstanding article “GraphQL, REST: Take the best of both” to gain a wider perspective on introducing GraphQL into your web API.
GraphQL Specs
The GraphQL specification says:
Generally speaking, directives provide a way to describe alternate runtime execution and type validation behavior in a GraphQL document.
It means that directives provide for altering GraphQL’s execution behavior, as well as describing additional information for types, fields, fragments and operations.
We will focus on how to use them in order to add visibility restrictions on certain fields.
The Schema
Consider the following GraphQL schema:
type User {
id: ID!
username: String!
email: String!
password: String
role: String!
}
type Employee {
id: ID!
firstName: String!
lastName: String!
salary: Float
}
type Query {
user(id: ID!): User
employee(id: ID!): Employee
}
It is a very simple schema, but looking a bit better we can spot a couple of things.
First, the field password
within the User
type appears in query responses and it shouldn’t, not even to administrative or super users, neither if the field is encrypted.
Similarly, the salary
field in the Employee
type should not be exposed to every user performing a query, but to administrators only.
This is where directives come into play. Let’s add two custom directives to our schema: @sc_hidden
and @sc_required_role
:
directive @sc_hidden on FIELD_DEFINITION
directive @sc_required_role(role: String!) on FIELD_DEFINITION
type User {
id: ID!
username: String!
email: String!
password: String @sc_hidden
role: String!
}
type Employee {
id: ID!
firstName: String! @deprecated
lastName: String!
salary: Float @sc_required_role(role: "administrator")
}
type Query {
user(id: ID!): User
employee(id: ID!): Employee
}
The first thing we can notice is that directives must be declared before they are used within the schema. As a good rule of thumb, position their declarations at the beginning.
Both directives are tied to the FIELD_DEFINITION location; this determines where the directive can be used within the schema, and in fact you can see we decorated the password
and the salary
fields in the respective types with the two directives.
The @sc_hidden
directive we define here will prevent a field value to be sent out in query responses.
This shows us a first alteration of the GraphQL schema behavior; this use case starts showing us exactly what directives are for.
Directives can even accept arguments, pretty much like functions do, as we can see into @sc_required_role
: the behavior we want to add to the schema is the decorated field’s value being accessible if and only if the user has the proper authorization role.
Cool, isn’t it? Well, it’s not all there yet, though. Defining the directives within the schema is not enough, now we must implement the associated behavior into our application.
Before proceeding, however, a word on directive names. We are using what the GraphQL specification calls custom directives. Thus, it is considered good practice to prefix their names with a short string giving readers the context of such a directive. For example, for Facebook GraphQL directives they may have been using fc_
. Similarly, we picked the prefix sc_
to indicate SpazioCodice.
Oh, and the underscore has a meaning as well: by the actual specification, built-in directives cannot use underscores in their names; this concurs in letting the next programmer reading the schema clearly understand they are custom directives and not built-in ones.
For a complete list of GraphQL directives’ features, see https://spec.graphql.org/October2021/#sec-Type-System.Directives.
Setup The Project
We are going to create our project as a Spring Boot application, based on Maven. You can as well create it from scratch using Spring Initializer (https://start.spring.io/).
We used Spring Boot 2.6.4 and we relied on the following dependencies in our pom.xml:

After configuring the POM, we will prepare a new file schema.graphqls
in src/main/resources
with the content indicated earlier (the one with directives declaration). In fact, graphql-java
will search for schema definitions in that directory first. However, keep in mind the schema can as well be defined in a Maven module per se, and included as a dependency in the current project.
It’s a very good and clean solution when your schema grows in complexity and you need to break it down in more than one single, monolithic file; having too many files in your resources directory, indeed, can be cluttering.
The Data Fetchers
As an initial step of our implementation we will create the data fetchers, one of the most important concepts for a GraphQL server, for that matter.
A DataFetcher fetches the data for one field while the query is executed. For simplicity, in this example, we gathered all the data fetchers in a single class:
@Component
public class GraphQLDataFetchers {
static final List USERS = asList(
Map.of(
"id", "1",
"username", "user1",
"email", "user1@mycompany.com",
"password", "Y6HhM|8?9,%X{w@dczg`",
"role", "user"),
Map.of(
"id", "2",
"username", "user2",
"email", "user2@mycompany.com",
"password", "_3KDuf5c6tgFGqmOueaF",
"role", "administrator")
);
static final List
EMPLOYEES = asList(
Map.of("id", "12345",
"firstName", "Jean",
"lastName", "Kowalski",
"salary", 52000.00F),
Map.of("id", "23456",
"firstName", "Martin",
"lastName", "Brewert",
"salary", 51000.00F),
Map.of("id", "34567",
"firstName", "Monique",
"lastName", "Duval",
"salary", 54000.00F)
);
public DataFetcher
user() {
return dataFetchingEnvironment -> {
var userId = dataFetchingEnvironment.getArgument("id");
return USERS
.stream()
.filter(user -> user.get("id").equals(userId))
.findFirst()
.orElse(null);
};
}
public DataFetcher
employee() {
return dataFetchingEnvironment -> {
var employeeId = dataFetchingEnvironment.getArgument("id");
return EMPLOYEES
.stream()
.filter(employee -> employee.get("id").equals(employeeId))
.findFirst()
.orElse(null);
};
}
}
Yeah, I can hear you: “how about a database?”. Indeed, we are getting our users and employees from static lists within the class.
This is one of the advantages of GraphQL: it doesn’t impose at all where the data comes from. The data can come from a database, an in-memory map, or a remote service. It doesn’t matter, as long as the needed data can be fetched.
On top of that, recall that in our schema we defined user()
and employee()
as members of the Query
supertype. Since they are defined as methods in our class, though, one could be tempted to consider them as methods or functions of Query
.
It’s worth noting that it’s not like that. GraphQL does not distinguish between fields and functions within the schema, they are all treated just as fields.
Implementing The Schema Loader
Right after defining the data fetchers, it is time to start with a schema loader implementation.
@Service
public class GraphQLService {
@Autowired
GraphQLDataFetchers graphQLDataFetchers;
private GraphQL graphQL;
@Bean
public GraphQL graphQL() {
return graphQL;
}
@PostConstruct
public void init() throws IOException {
URL url = Resources.getResource("schema.graphqls");
String sdl = Resources.toString(url, Charsets.UTF_8);
GraphQLSchema graphQLSchema = buildSchema(sdl);
this.graphQL = GraphQL.newGraphQL(graphQLSchema).build();
}
private GraphQLSchema buildSchema(String sdl) {
TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(sdl);
RuntimeWiring runtimeWiring = buildWiring();
SchemaGenerator schemaGenerator = new SchemaGenerator();
return schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring);
}
private RuntimeWiring buildWiring() {
return RuntimeWiring.newRuntimeWiring()
.directive("sc_required_role", new RequiredRoleDirective())
.directive("sc_hidden", new HiddenDirective())
.type(newTypeWiring("Query")
.dataFetcher("user", graphQLDataFetchers.user()))
.type(newTypeWiring("Query")
.dataFetcher("employee", graphQLDataFetchers.employee()))
.build();
}
}
We packaged the schema loader in a dedicated service, annotating the class with @Service
. The init()
method here is responsible for properly building the new GraphQL object that will be available throughout the whole application. The object is bound to the schema built with the buildSchema()
method, which performs a formal validation on the provided schema and makes it executable. init()
is needed to be invoked as soon as the GraphQLService
object has been constructed, thus we annotated the method with @PostConstruct
.
buildSchema()
method, in turn, calls the buildWiring()
method, responsible for delivering a RuntimeWiring
object. RuntimeWiring
object is a specification of data fetchers, type resolvers and custom scalars that are needed to wire together a working GraphQLSchema
object. In synthesis, it pulls together all the several parts composing the schema as it has been defined and as we expect it to be. buildWiring()
method, we wire together the directives we defined and our two DataFetcher’s, all conveniently wrapped in a builder interface. As a bottom line, we could say the schema loader is the backbone of the GraphQL part of an application.
Authentication + Authorization
In order to support our example from now on, we would need to add authentication functionality. Which, by the way, would add a lot of boilerplate code to our tiny little world.
In fact, under normal circumstances, an object of some type, representing the authorization details coming from the authentication phase, should be injected into the GraphQLContext
. Usually, such an object is available into the web request as an attribute (e.g. webRequest.getAttribute("<attribute name>", RequestAttributes.SCOPE_REQUEST)
).
As a side note, GraphQLContext
can be considered as a map containing key values, useful when executing data fetchers.
However, for the sake of simplicity within our example, we are using the AuthHelper
class to provide that object:
@Getter
@Setter
public class AuthHelper {
private User authenticatedUser;
public boolean userHasRole(final String role) {
return authenticatedUser.getRole().equals(role);
}
public AuthHelper(final String username) {
switch (username) {
case "user1":
authenticatedUser = User.builder()
.username("user1")
.email("user1@mycompany.com")
.role("user").build();
break;
case "user2":
authenticatedUser = User.builder()
.username("user2")
.email("user2@mycompany.com")
.role("administrator").build();
}
}
}
userHasRole()
method. GraphQLContext
: using (as a Bean) a GraphQL ExecutionInputCustomizer
. For that purpose, we add a GraphQLConfiguration
class, exposing the method returning the customizer:
With this class, loaded as a @Configuration
object by Spring Boot, we can provide an execution input customizer Bean that injects an “auth” property into the GraphQL context. Remember that “user1” as a username in our example identifies the regular user. NOTE: To test different behaviors related to different roles we can change the username here to “user2” and restart the application.
@Configuration
public class GraphQLConfiguration {
@Bean
public ExecutionInputCustomizer executionInputCustomizer() {
return (executionInput, webRequest) -> {
executionInput.getGraphQLContext()
// Here, the Authorization object that is put into the
// GraphQLContext should come from a WebRequest's attribute:
//
// .put("auth", webRequest.getAttribute("attribute name",
// RequestAttributes.SCOPE_REQUEST))
// However, for the sake of simplicity within our example,
// we are using the AuthHelper class to provide that object.
.put("auth", new AuthHelper("user1"));
return CompletableFuture.completedFuture(executionInput);
};
}
}
Directives Implementation
And finally, here we go: we’re implementing our custom directives.
Let’s start slow and easy with the simpler directive of ours, HiddenDirective.
@sc_hidden
aims to protect those fields that must not be seen by anyone, regardless the role. @sc_hidden
name and our HiddenDirective
class happens into the GraphQLService::buildWiring()
method:
return RuntimeWiring.newRuntimeWiring()
.directive("sc_required_role", new RequiredRoleDirective())
Now, let’s take a look at the directive class:
public class HiddenDirective implements SchemaDirectiveWiring {
@Override
public GraphQLFieldDefinition onField(
SchemaDirectiveWiringEnvironment environment) {
GraphQLFieldDefinition field = environment.getElement();
GraphQLFieldsContainer parentType = environment.getFieldsContainer();
DataFetcher hiddenFieldDataFetcher = context -> null;
environment.getCodeRegistry().dataFetcher(parentType, field, hiddenFieldDataFetcher);
return field;
}
}
In order to be treated as a GraphQL directive, the class must implement the SchemaDirectiveWiring
interface. The documentation tells us that a SchemaDirectiveWiring is responsible for enhancing a runtime element based on directives placed on that element in the schema. It can enhance that GraphQL runtime element and add new behavior. We relied on this feature, by using it to change a field’s data fetcher.
To do that, we have overridden the onField()
method.
In the first place, we get the field definition object by means of the SchemaDirectiveEnvironment
‘s method getElement()
. Look at how this method is general in its nature by depending on the used generics.
We then extract the field’s container with the environment’s getFieldsContainer()
method. We will need the container object later.
Subsequently, we come to the real core and purpose of the directive, instantiating an object implementing the DataFetcher interface, hiddenFieldDataFetcher
. The long-winded way to achieve that data fetcher would have been:
DataFetcher hiddenFieldDataFetcher = new DataFetcher() {
@Override
public Object get(DataFetchingEnvironment context) {
return null;
}
};
The get()
method in there, although receiving a complete DataFetchingEnvironment
(i.e. all the context we need to fetch the value), only returns null
, just as the @sc_hidden
directive requires.
Anyways, looking a bit better at the construct, we realize it can be resolved to a lambda:
DataFetcher> hiddenFieldDataFetcher = context -> null;
DataFetcher
with the environment, returning the wanted field in the end. We can see that, in its structure, a custom directive is quite easy to implement, once we get acquainted with the objects provided by the GraphQL framework.
RequiredRoleDirective
class.
public class RequiredRoleDirective implements SchemaDirectiveWiring {
@Override
public GraphQLFieldDefinition onField(
SchemaDirectiveWiringEnvironment environment) {
var requiredAuthRole =
Optional.of(((StringValue) environment.getDirective()
.getArgument("role")
.getArgumentValue()
.getValue()).getValue())
.filter(role -> List.of("user", "administrator").contains(role))
.map(String::valueOf)
.orElseThrow(() -> new GraphQLException("Unknown role argument"));
GraphQLFieldDefinition field = environment.getElement();
var parentType = environment.getFieldsContainer();
DataFetcher originalDataFetcher =
environment.getCodeRegistry().getDataFetcher(parentType, field);
DataFetcher authDataFetcher = context -> {
var contextMap = context.getGraphQlContext();
AuthHelper authContext = contextMap.get("auth");
return (authContext.userHasRole(requiredAuthRole))
? originalDataFetcher.get(context)
: null;
};
environment.getCodeRegistry().dataFetcher(parentType, field, authDataFetcher);
return field;
}
}
In principle, there are two main differences with the previous HiddenDirective.
role
argument value, here represented by the requiredAuthRole
variable. To get to it, we need to pass through the directive definition and get the value for the argument, as it has been defined within the schema.
role
argument of the @sc_required_role
directive, we fail in processing the request with an exception. We are forced to take care of that aspect, as the schema loader performs a formal validation of the schema ,from a syntactic point of view.
DataFetcher
. DataFetcher::get()
method, we extract from the GraphQL context the AuthHelper
object previously injected. null
will be returned to protect the value from unwanted eyes. Summary
The source code for the complete example used in this article is available on SpazioCodice’s GitHub repository https://github.com/spaziocodice/blog-graphql-directives-101.
GraphQL directives represent a really good and structured way to protect content in our GraphQL based web applications, but we must not forget they can do much more. From values formatting to data transformation, literally anything we can do with data provided by data fetchers can be a subject to GraphQL directives.
I hope this GraphQL directives appetizer did turn on your interest, stay tuned on spaziocodice.com for more articles on programming.
Happy coding!