GraphQL Server Using Spring Boot, Part II — Scalars

Balázs Vajner
Supercharge's Digital Product Guide
6 min readJul 14, 2020

In the first part of this series, we created a simple “Hello World”-style application. In this second part, we start to go deeper into the GraphQL type system.

Overview

It is important to get familiar with the GraphQL type system, as it has some aspects that you might find strange when coming from Java. In GraphQL, for example, there is a strict separation between Types used as return values, and Inputs used as parameters.

In the first part, we already used predefined scalars and our own types. This part is about how to create our own custom scalars. The next part of the series will discuss the rest of the type system (Enum, Input, Interface and Union).

About scalars

GraphQL scalars are atomic (indivisible) values, meaning that they can only be selected as a whole and cannot be divided into multiple fields. For example, you cannot select the length or only the first character of a string. Each execution path in GraphQL operations must end with selecting exactly one scalar value. Thus, it is impossible to write a GraphQL operation without scalars being involved.

Note: there is no void in GraphQL. Even mutations equivalent to REST APIs returning 204 No content must return something.

In graph theory terms, a GraphQL operation can be considered a rooted tree and scalars can be considered the leaves of that tree.

Out-of-the-box scalars

In addition to the standard GraphQL scalars (Int, Float ,Boolean ,String,ID ) GraphQL Java — the low-level implementation of the specification used under the hood — defines the following Java-specific scalars: BigDecimal, BigInteger Byte,Char ,Short,Long.

To use these scalars in our schema, we only have to declare them:

Declaring GraphQL-Java specific scalars

The corresponding Java classes can now be used in our API without any further coding or configuration.

Note: The latest major version of GraphQL Java (15.0) changes the way of handling non-standard scalars. Theses scalars are no longer provided out of the box, instead are externalised to a separate library. The current version of the Spring Boot starter used here builds upon GraphQL Java 14.1.

Defining custom scalars

It is, of course, possible to define our own scalars. Let’s add a dateOfBirth field to the Person type defined in the first part of this series! The appropriate Java type for that field is LocalDate. However, there is no such predefined scalar. What can we do?

A quick and dirty shortcut

This problem can be solved quickly by using LocalDate on the Java side, and String on the GraphQL side. Under the hood, the convertValue method of Jackson’s ObjectMapper will be used to convert the String to an instance of the specific Java class. This trick works for any class that can be converted by Jackson. If Jackson fails to convert the value, an exception will be thrown and the operation will fail.

Whilst this method is workable to some extent, it has some notable drawbacks:

  • on the GraphQL side, a generic String or Int type may confuse the consumer of the API and defeats one of the core benefits of GraphQL: type safety
  • there is no control over the conversion process (apart of customising Jackson itself).
  • it is limited to the capabilities of the aforementioned Jackson method (although it is quite powerful)
  • it is dependent on Jackson’s configuration.

The proper implementation

To address these issues, a custom GraphQL scalar must be defined. Let’s add the a Date field to the Person type:

Person type with dateOfBirth added.

Since there is no Date type or scalar yet, it must be explicitly declared:

Date scalar declaration

In our code, a private LocalDate dateOfBirth; field must also be added to the Person class (with getter and setter, of course). The constructor of the class, as well as the createPerson mutation resolver — both in code and schema — must also be extended with a new dateOfBirth parameter.

The final step in the process is the definition of the GraphQL scalar itself. This can be done by providing a GraphQLScalarType bean:

The provided name must match the name declared in the schema. The description is optional but useful for API consumers, as it can provide further documentation about the scalar. The description will also appear in GraphQL or other similar tools that can visualise the GraphQL schema.

Note: our new scalar is called Date, but we use the LocalDate Java class to represent it. Since the match between the Java class and the GraphQL scalar is explicitly defined, there is no need to use the same name.

The most important part of the scalar definition is the so-called coercing. On the Java side, it is possible to use any class we want, but ultimately, it must be serialised to/deserialised form a JSON data type/basic GraphQL scalar.

The logic of these processes are defined by an implementation of the graphql.schema.Coercing<I, O> interface. The first type parameter, I (for input) must be the Java class, and the O (for output) must be a class corresponding to a standard GraphQL scalar/JSON data type (Integer, Float, Boolean or String). The class must implement the following three methods of the interface:

O serialize(Object dataFetcherResult) throws CoercingSerializeException

The name largely speaks for itself —the method must serialise its input to the declared output type. Note that dataFetcherResult is not of type I, as it cannot be guaranteed that the data fetching result (i. e. the value returned by a resolver method) is actually of type I. Although, it usually should be. At least it is guaranteed to be a non-null value. (Nulls are always serialised as null.) The implementation checks if the data fetching result is actually a LocalDate instance and throws an exception if it is not.

The other two methods are responsible for coercing in the opposite direction. Since there are two ways to provide input values for a GraphQL operation, there are two different methods that deal with the coercing:

I parseLiteral(Object input) throws CoercingParseLiteralException

parseLiteral will be used if the input is provided “inline”, that is, as part of the GraphQL operation. In this case, input will be a node from the abstract syntax tree (AST) representation of the GraphQL operation. For example, in case of a String, input will be a graphql.language.StringValue instance.

createPerson mutation example, that will use the parseLiteral method

I parseValue(Object input) throws CoercingParseValueException

parseValue will be used if the input variables are provided as a standalone variables JSON. In this case, input will be a Java type representing a basic JSON type (String in the example).

Create person mutation using external variables
The variables JSON

With this definition in place, all is set to use the Date scalar in our schema.

Additional notes

Finally, there are a few not-so-obvious details that are worth mentioning.

Error handling in coercing

Each coercing method declares a specific exception type that it should throw. It is important to adhere to this specification and only throw that kind of exceptions. Other exception types (including runtime exceptions) may cause malfunction and result in an invalid response, e. g. 400 Bad Request with an empty response body, instead of a specification-compliant GraphQL response containing an error (see example below). Other exceptions must be caught and wrapped in the required exception type.

Example error response if exception handling is correctly set up for a custom scalar.

Note: an upcoming part of this series will be dedicated to error handling.

Type safety and coercing

Notice that the arguments of the serialise and parse methods are declared as Object. This means that it is necessary to check the actual type with instanceof. Also, this opens up the possibility to handle multiple types, e. g. a date could be represented as an ISO string or an integer timestamp value. Although possible, this feature is to be used with caution. It may be preferable that scalars have one well defined format, and everything else is considered invalid.

At Supercharge, we are a next-generation innovation partner working with our clients to create transformative digital solutions. If you liked this article, check out some of Supercharge’s other articles on our blog, and follow us on LinkedIn, and Facebook. If you’re interested in open positions, follow this link.

--

--