Nov 14th 2017

How to design GraphQL custom scalars

Introduction

As we discussed in this article, in GraphQL specification we have different built-in scalar types. But what can we do if we need to use scalars which are not defined in the GraphQL specification? The answer is to use the so-called “custom scalar”. As in the previous articles we will use graphql-js. The implementation of the GraphQL custom scalar can vary across other GraphQL implementations. In some of these implementations, it may not be even possible to use custom scalars.

We will continue with the same repository as with the enums and built-in scalars. You can clone the GitHub repository using this command

git clone https://github.com/a7v8x/express-graphql-demo.git -b feature/3-graphql-scalars

Custom scalar definition

Now let’s go right into the designing of the DateTime scalar. We will use validator-js library to test if the value is in ISO8601 date time string format. The simplified definition of the DateTime scalar can be as follows

import { GraphQLScalarType } from 'graphql';

import { isISO8601 } from 'validator';

const parseISO8601 = (value) => {
  if (isISO8601(value)) {
    return value;
  }
  throw new Error('DateTime cannot represent an invalid ISO-8601 Date string');
};

const serializeISO8601 = (value) => {
  if (isISO8601(value)) {
    return value;
  }
  throw new Error('DateTime cannot represent an invalid ISO-8601 Date string');
};

const parseLiteralISO8601 = (ast) => {
  if (isISO8601(ast.value)) {
    return ast.value;
  }
  throw new Error('DateTime cannot represent an invalid ISO-8601 Date string');
};

const DateTime = new GraphQLScalarType({
  name: 'DateTime',
  description: 'An ISO-8601 encoded UTC date string.',
  serialize: serializeISO8601,
  parseValue: parseISO8601,
  parseLiteral: parseLiteralISO8601,
});

export { DateTime as default };

In graphql-js, each custom scalar type definition has different fields and methods that should be defined. Just as with input/output object types, we have to define the required field name of the scalar. The description field is once again mandatory. If you read the article on built-in scalars we went through input and result coercion for each type. This is defined in graphql-js library for built-in scalars. However, when we define our own custom scalar, we have to specify these rules. That is why we have to define a couple of methods for each custom scalar. These are:

Serialize

The serialize function refers to the result coercion. The first argument for this function is the received value itself. In our case, we would like to check if the value is in ISO8601 date format. If the received value passes the validation, we want to return this value; otherwise, we would raise the GraphQL error.

ParseValue and parseLiteral

The parseValue and parseLiteral function refers to the input coercion. The difference is that in parseValue, we refer to the input passed using the variables. When we use parseValue, the first argument of this function is the value itself. On the other hand, the parseLiteral function has its first argument the ast value in the following format

{ "kind": "StringValue",
"value": "2017-10-06T14:54:54+00:00",
"loc": { "start": 51, "end": 78 } }

That is why we have to extract the value from the ast variable and again validate it by our rules.

Implementation

We have completed the definition of our custom scalar, and therefore it is possible to implement it in the schema. We will apply it on the timestamp fields createdAt and updatedAt. We can simply import our Date Time scalar and use it as a type for these fields.

import {
  GraphQLString,
  GraphQLID,
  GraphQLObjectType,
  GraphQLNonNull,
  GraphQLInt,
  GraphQLFloat,
  GraphQLBoolean,
} from 'graphql';

import DateTimeScalar from '../scalars/dateTimeScalar';
import TaskStateEnumType from './taskStateEnumType';

const taskType = new GraphQLObjectType({
  name: 'Task',
  fields: () => ({
    id: {
      type: new GraphQLNonNull(GraphQLID),
    },
    name: {
      type: new GraphQLNonNull(GraphQLString),
    },
    completed: {
      type: new GraphQLNonNull(GraphQLBoolean),
    },
    state: {
      type: new GraphQLNonNull(TaskStateEnumType),
    },
    progress: {
      type: new GraphQLNonNull(GraphQLFloat),
    },
    taskPriority: {
      type: new GraphQLNonNull(GraphQLInt),
    },
    createdAt: {
      type: new GraphQLNonNull(DateTimeScalar),
    },
    updatedAt: {
      type: new GraphQLNonNull(DateTimeScalar),
    },
  }),
});

export {
  taskType as default,
};

Now let’s check out if our rules, which we defined works as expected. Just go to /graphiql and try to call the basic getTasks query:

query getTasks {
  tasks {
    id
    name
    taskPriority
    progress
    createdAt
    updatedAt
  }
}

If our custom Date Time scalar is implemented correctly, you should get something like this:

{
  "data": {
    "tasks": [
      {
        "id": "7e68efd1",
        "name": "Test task",
        "taskPriority": 1,
        "progress": 55.5,
        "createdAt": "2017-10-06T14:54:54+00:00",
        "updatedAt": "2017-10-06T14:54:54+00:00"
      }
    ]
  }
}

Now let’s test out our serialize function. Just go to the taskDb.js file and reassign createdAt or updatedAt to a string which is not in ISO8601 date format. We will change the createdAt field to 2017–10–06T14:54:54+0. Now if we try to call the getTasks Query, the GraphQL server will raise the following error:

{
"data": {
  "tasks": [
    null
  ]
},
"errors": [
  {
    "message": "DateTime cannot represent an invalid ISO-8601 Date instance",
    "locations": [
      {
        "line": 7,
        "column": 5
      }
    ],
    "path": [
      "tasks",
      0,
      "createdAt"
    ]
  }
]
}

Summary

Custom scalars provide an even greater benefit when we have a collection of our predefined custom scalars. Then we can centralize most of our custom validations and move them from our resolver functions to our custom scalar types. Our simple Date Timescalar was a great model example. However, if you want to use more complex scalars like JSON or more precisely defined Date Time scalars, etc., it is possible to use some of the open sourced npm packages. These include:

  • JSON: Using JSON as a scalar https://github.com/taion/graphql-type-json.
  • Date Time: It is a library you can use for more precisely defined Date Time scalar. In this library, seperate Time and Date scalars are also available https://github.com/excitement-engineer/graphql-iso-date.
  • Date Time and email: This package is a small collection of custom scalars https://github.com/okgrow/graphql-scalars.
Did you like this post? The repository with the examples and project setup can be cloned from this branch . Feel free to send any questions about the topic to david@atheros.ai.

Join thousands of others and be occasionally notified about new articles & news from engineering world.

* Signing up for GraphQL Mastery newsletter indicates you agree with Terms and Conditions and Privacy Policy including our Cookie Policy.