1. Introduction
GraphQL is a powerful query language for APIs and provides a flexible and efficient way to interact with our data. When dealing with mutations, it’s typical to perform updates or additions to data on the server. However, in some scenarios, we might need to mutate without returning any data.
In GraphQL, the default behavior is to enforce non-nullability for fields in the schema, meaning that a field must always return a value and cannot be null unless explicitly marked as nullable. While this strictness contributes to the clarity and predictability of the API, there are instances where returning null might be necessary. However, it’s generally considered a best practice to avoid returning null values.
In this article, we’ll explore techniques for implementing GraphQL mutations without retrieving or returning specific information.
2. Prerequisites
For our example, we’ll need the following dependencies:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-graphql</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
The Spring Boot GraphQL Starter provides an excellent solution for quickly setting up a GraphQL server. By leveraging auto-configuration and adopting an annotation-based programming approach, we only need to focus on writing the essential code for our service.
We’ve included the web starter in our config because GraphQL is transport-agnostic. This utilizes Spring MVC to expose the GraphQL API over HTTP. We can access this via the default /graphql endpoint. We can also use other starters, like Spring Webflux, for different underlying implementations.
3. Using Nullable Type
Unlike some programming languages, GraphQL mandates an explicit declaration of nullability for each field in the schema. This approach enhances clarity, allowing us to convey when a field may lack value.
3.1. Writing the Schema
The Spring Boot GraphQL starter automatically locates GraphQL Schema files under the src/main/resources/graphql/** location. It builds the correct structure based on them, and wires special beans to this structure.
We’ll start by creating the schema.graphqls file, and defining the schema for our example:
type Post {
id: ID
title: String
text: String
category: String
author: String
}
type Mutation {
createPostReturnNullableType(title: String!, text: String!, category: String!, authorId: String!) : Int
}
We’ll have a Post entity and a mutation to create a new post. Also, for our schema to pass validation, it must have a query. So, we’ll implement a dummy query that returns a list of posts:
type Query {
recentPosts(count: Int, offset: Int): [Post]!
}
3.2. Using Beans to Represent Types
In the GraphQL server, every complex type is associated with a Java bean. These associations are established based on the object and property names. That being said, we’ll create a POJO class for our posts:
public class Post {
private String id;
private String title;
private String text;
private String category;
private String author;
// getters, setters, constructor
}
Unmapped fields or methods on the Java bean are overlooked within the GraphQL schema, posing no issues.
3.3. Creating the Mutation Resolver
We must mark the handler functions with the @MutationMapping tag. These methods should be placed within regular @Controller components in our application, registering the classes as data-modifying components in our GraphQL application:
@Controller
public class PostController {
List<Post> posts = new ArrayList<>();
@MutationMapping
public Integer createPost(@Argument String title, @Argument String text, @Argument String category, @Argument String author) {
Post post = new Post();
post.setId(UUID.randomUUID().toString());
post.setTitle(title);
post.setText(text);
post.setCategory(category);
post.setAuthor(author);
posts.add(post);
return null;
}
}
We must annotate the parameters of the method with @Argument according to the properties from the schema. When declaring the schema, we determined that our mutation would return an Int type, without the exclamation mark. This allowed the return value to be null.
4. Creating Custom Scalar
4.1. Scalars and Extended Scalars
According to the GraphQL specification, all implementations must include the following scalar types: String, Boolean, Int, Float, or ID. Besides that, graphql-java-extended-scalars adds more custom-made scalars like Long, BigDecimal, or LocalDate. However, neither the original nor the extended set of scalars have a special one for the null value. So, we’ll build our scalar in this section.
4.2. Creating the Custom Scalar
To create a custom scalar, we should initialize a GraphQLScalarType singleton instance. We’ll utilize the Builder design pattern to create our scalar:
public class GraphQLVoidScalar {
public static final GraphQLScalarType Void = GraphQLScalarType.newScalar()
.name("Void")
.description("A custom scalar that represents the null value")
.coercing(new Coercing() {
@Override
public Object serialize(Object dataFetcherResult) {
return null;
}
@Override
public Object parseValue(Object input) {
return null;
}
@Override
public Object parseLiteral(Object input) {
return null;
}
})
.build();
}
The key components of the scalar are name, description, and coercing. Although the name and description are self-explanatory, the hard part of creating a custom scalar is graphql.schema.Coercing implementation. This class is responsible for three functions:
- parseValue(): accepts a variable input object and transforms it into the corresponding Java runtime representation
- parseLiteral(): receives an AST literal graphql.language.Value as input and transform it into the Java runtime representation
- serialize(): accepts a Java object and converts it into the output shape for that scalar
Although the implementation of coercing can be quite complicated for a complex object, in our case, we’ll return null for each method.
4.3. Register the Custom Scalar
We’ll start by creating a configuration class where we register our scalar:
@Configuration
public class GraphQlConfig {
@Bean
public RuntimeWiringConfigurer runtimeWiringConfigurer() {
return wiringBuilder -> wiringBuilder.scalar(GraphQLVoidScalar.Void);
}
}
We create a RuntimeWiringConfigurer bean where we configure the runtime wiring for our GraphQL schema. In this bean, we use the scalar() method provided by the RuntimeWiring class to register our custom type.
4.4. Integrate the Custom Scalar
The final step is to integrate the custom scalar into our GraphQL schema by referencing it using the defined name. In this case, we use the scalar in the schema by simply declaring scalar Void.
This step ensures that the GraphQL engine recognizes and utilizes our custom scalar throughout the schema. Now, we can integrate the scalar into our mutation:
scalar Void
type Mutation {
createPostReturnCustomScalar(title: String!, text: String!, category: String!, authorId: String!) : Void
}
Also, we’ll update the mapped method signature to return our scalar:
public Void createPostReturnCustomScalar(@Argument String title, @Argument String text, @Argument String category, @Argument String author)
5. Conclusion
In this article, we explored implementing GraphQL mutations without returning specific data. We demonstrated setting up a server quickly with the Spring Boot GraphQL Starter. Furthermore, we introduced a custom Void scalar to handle null values, showcasing how to extend GraphQL’s capabilities.
As always, the complete code snippets are available over on GitHub.