Custom Scalars in GraphQL

Mirko Nasato, September 17, 2018

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 DateTime

type 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:

Schema Documentation

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.