GraphQL Server Using Spring Boot, Part II — Scalars
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:
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
orInt
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:
Since there is no Date
type or scalar yet, it must be explicitly declared:
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.
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).
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.
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.