Custom Scalars in GraphQL
GraphQL provides a small set of predefined scalar types: Boolean
, ID
, Int
, Float
, and String
. But we can define our own custom scalar types as well. In this post we'll see how to do that, with an example based on Apollo Server.
A common use for custom types is representing dates and times. There are different ways to encode a date/time value: as an ISO-8601 string, e.g. 2018-09-16T17:27:33.963Z
. Or as Unix timestamp, i.e. a number like 1537118853
that represents the seconds elapsed since the epoch (01/01/1970). Or again as milliseconds since epoch, e.g. 1537118853231
.
Letâs say we want to use ISO-8601, because itâs a standard and itâs also more human readable.
Schema
We could declare a schema like this (assuming our whole API consists of a single query that returns a time
value):
type Query {
time: String
}
But that doesnât really tell our clients that the String
we return is not just any string, it's an ISO-8601 string representing a date/time value.
Hereâs how we can declare and use a custom scalar type called DateTime
instead:
scalar DateTimetype Query {
time: DateTime
}
This way we make it clear that time
is of type DateTime
.
Admittedly, looking at the type definition above we cannot really tell that a DateTime
is represented as an ISO-8601 string. But we'll take care of that when we get to implementing our custom scalar implementation. We'll provide a description that will be displayed in the schema documentation.
The Github GraphQL API for example defines its own DateTime scalar, and its documentation explains that itâs âan ISO-8601 encoded UTC date stringâ.
Implementation
Defining a custom scalar type in the schema is not enough. We also need to tell the GraphQL engine how to convert values of that type from the internal representation used in our code when writing a response or reading a request.
For example we may use a JavaScript Date
object in our code to represent a date/time, but when generating a GraphQL response we want to convert the JavaScript Date
into an ISO-8601 string.
Letâs see how that works in JavaScript with Apollo Server.
As a starting point for our example weâll use the basic GraphQL server and client projects in the mirkonasato/graphql-examples Github repository. So if you want to try the code on your machine go and git clone
that repository, then follow the setup instructions in the README.md
file.
In the server
project we can start by changing the typeDefs
value in server.js
to be the same schema we defined above, i.e.:
const typeDefs = gql`
scalar DateTime
type Query {
time: DateTime
}
`;
Then we can change the resolvers
object to respond to a time
query by returning a new Date object:
const resolvers = {
Query: {
time: () => new Date()
}
};
And finally we get to the meat: our custom DateTime
implementation. This also goes into the resolvers
object, just like any other custom type and field declared in our schema:
const { GraphQLScalarType } = require('graphql');
const typeDefs = /*â¦*/;
const resolvers = {
DateTime: new GraphQLScalarType({
name: 'DateTime',
description: 'A date and time, represented as an ISO-8601 string',
serialize: (value) => value.toISOString(),
parseValue: (value) => new Date(value),
parseLiteral: (ast) => new Date(ast.value)
}),
Query: /*â¦*/
};
We use the GraphQLScalarType class from the core graphql
library to create a new custom scalar.
We pass name
and description
, that should be self-explanation. The description
is where we can document that a DateTime
value is an ISO-8601 string.
Then we need to tell the GraphQL engine how to serialize
a value, i.e. convert it from a JavaScript Date
object into an ISO-8601 string when writing a reponse. Since we know the value
is a JavaScript Date
we can simply use its toISOString method.
Similarly, we need to provide the logic for parsing a value, i.e. reading it from a GraphQL request where it will be encoded as an ISO-8601 string, and converting it to a JavaScript Date
object to be used internally in our code.
For this case we actually need to provide two different functions: parseValue
that receives the raw value, i.e. an ISO-8601 string, and parseLiteral
that receives an abstract syntax tree (AST) object instead. The AST is generated by the GraphQL engine when parsing a request.
In both cases we can create a new Date
object by passing the value
as a constructor argument, since the Date constructor can accept an ISO-8601 string as parameter.
Thatâs the minimal amount of code required to get our example working. Ideally we should make our code more robust by checking that value
is actually of the expected type and throwing an error if it isn't, etc. But we'll keep it simple for this example.
For real-world usage, the @okgrow/graphql-scalars library provides a number of ready-made custom scalars, including a more complete DateTime implementation.
Playground
If we start the server and open the GraphQL Playground at localhost:9000 we should now see our DateTime
scalar in the schema documentation explorer:
At this point we can send a query:
query {
time
}
And we should receive a reponse like:
{
"data": {
"time": "2018-09-17T10:46:55.328Z"
}
}
This shows that the GraphQL response contains an ISO-8601, even though in our time
resolver function we return a JavaScript Date
object. Our DateTime
scalar is performing the conversion.
Client
So thatâs the server updated. What about the client?
By default, clients will receive the time
field as a string, as we can see in the JSON response above.
To convert the string into a JavaScript Date
object we'd need a to plug our custom DateTime
implementation into our client code as well. In fact, we could have clients written in other languages, like Java for Android and Swift for iOS. So those clients would need their own language-specific custom scalar implementation. For example, a Java version may convert the string into a java.util.Date
instance.
Thatâs the downside to using custom scalars. Theyâre not supported out of the box, we need to make our custom logic available to any projects that want to use them. Or simply let clients receive DateTime
values as plain strings.
The sample repository includes a simple React app that uses Apollo Client. As it turns out, Apollo Client doesnât currently support custom scalars (issue apollo-feature-requests#2). So weâll have to receive our time
value as a string and convert it manually when we receive a response. For example in src/queries.js
have:
export async function getTime() {
const {data: {time}} = await client.query({
query: gql`
query TimeQuery {
time
}
`
});
return new Date(time);
}
The full code for this example is available in the custom-scalar branch, and you can also look at the relevant changes only.
Conclusion
Is it worth using custom scalars, if each client still needs its own logic to interpret them correctly? Or is it better to stick with the predefined scalar types? Thatâs a design decision for you or your team to make.
Personally I think it is valuable for the server to use custom scalars in its schema in any case, to give a more meaningful representation of its API.
Originally published at encoded.io.