Custom scalars and enums
Add custom scalar and enum types to a schema.
The GraphQL specification includes the following default scalar types: Int
, Float
, String
, Boolean
and ID
. While this covers most of the use cases, some need to support custom atomic data types (e.g. Date
), or add validation to an existing type. To enable this, GraphQL allows custom scalar types. Enumerations are similar to custom scalars with the limitation that their values can only be one of a pre-defined list of strings.
Custom scalars
To define a custom scalar, add it to the schema string with the following notation:
scalar MyCustomScalar
Afterwards, define the behavior of a MyCustomScalar
custom scalar by passing an instance of the GraphQLScalarType
class in the resolver map. This instance can be defined with a dependency or in source code.
For more information about GraphQL's type system, please refer to the official documentation or to the Learning GraphQL tutorial.
Note that Apollo Client does not currently have a way to automatically interpret custom scalars, so there's no way to automatically reverse the serialization on the client.
Using a package
Here, we'll take the graphql-type-json package as an example to demonstrate what can be done. This npm package defines a JSON GraphQL scalar type.
Add the graphql-type-json
package to the project's dependencies :
$ npm install graphql-type-json
In code, require the type defined by in the npm package and use it :
const { ApolloServer, gql } = require('apollo-server');
const GraphQLJSON = require('graphql-type-json');
const schemaString = gql`
scalar JSON
type Foo {
aField: JSON
}
type Query {
foo: Foo
}
`;
const resolveFunctions = {
JSON: GraphQLJSON
};
const server = new ApolloServer({ typeDefs: schemaString, resolvers: resolveFunctions });
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`)
});
Remark : GraphQLJSON
is a GraphQLScalarType
instance.
Custom GraphQLScalarType
instance
Defining a GraphQLScalarType instance provides more control over the custom scalar and can be added to Apollo server in the following way:
const { ApolloServer, gql } = require('apollo-server');
const { GraphQLScalarType, Kind } = require('graphql');
const myCustomScalarType = new GraphQLScalarType({
name: 'MyCustomScalar',
description: 'Description of my custom scalar type',
serialize(value) {
let result;
// Implement custom behavior by setting the 'result' variable
return result;
},
parseValue(value) {
let result;
// Implement custom behavior here by setting the 'result' variable
return result;
},
parseLiteral(ast) {
switch (ast.kind) {
case Kind.Int:
// return a literal value, such as 1 or 'static string'
}
}
});
const schemaString = gql`
scalar MyCustomScalar
type Foo {
aField: MyCustomScalar
}
type Query {
foo: Foo
}
`;
const resolverFunctions = {
MyCustomScalar: myCustomScalarType
};
const server = new ApolloServer({ typeDefs: schemaString, resolvers: resolverFunctions });
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`)
});
Custom scalar examples
Let's look at a couple of examples to demonstrate how a custom scalar type can be defined.
Date as a scalar
The goal is to define a Date
data type for returning Date
values from the database. Let's say we're using a MongoDB driver that uses the native JavaScript Date
data type. The Date
data type can be easily serialized as a number using the getTime()
method. Therefore, we would like our GraphQL server to send and receive Date
s as numbers when serializing to JSON. This number will be resolved to a Date
on the server representing the date value. On the client, the user can simply create a new date from the received numeric value.
The following is the implementation of the Date
data type. First, the schema:
const typeDefs = gql`
scalar Date
type MyType {
created: Date
}
`
Next, the resolver:
const { GraphQLScalarType } = require('graphql');
const { Kind } = require('graphql/language');
const resolvers = {
Date: new GraphQLScalarType({
name: 'Date',
description: 'Date custom scalar type',
parseValue(value) {
return new Date(value); // value from the client
},
serialize(value) {
return value.getTime(); // value sent to the client
},
parseLiteral(ast) {
if (ast.kind === Kind.INT) {
return parseInt(ast.value, 10); // ast value is always in string format
}
return null;
},
}),
};
const server = new ApolloServer({ typeDefs, resolvers });
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`)
});
Validations
In this example, we follow the official GraphQL documentation for the scalar datatype, which demonstrates how to validate a database field that should only contain odd numbers in GraphQL. First, the schema:
const typeDefs = gql`
scalar Odd
type MyType {
oddValue: Odd
}
`
Next, the resolver:
const { ApolloServer, gql } = require('apollo-server');
const { GraphQLScalarType } = require('graphql');
const { Kind } = require('graphql/language');
function oddValue(value) {
return value % 2 === 1 ? value : null;
}
const resolvers = {
Odd: new GraphQLScalarType({
name: 'Odd',
description: 'Odd custom scalar type',
parseValue: oddValue,
serialize: oddValue,
parseLiteral(ast) {
if (ast.kind === Kind.INT) {
return oddValue(parseInt(ast.value, 10));
}
return null;
},
}),
};
const server = new ApolloServer({ typeDefs, resolvers });
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`)
});
Enums
An Enum is similar to a scalar type, but it can only be one of several values defined in the schema. Enums are most useful in a situation where the user must pick from a prescribed list of options. Additionally enums improve development velocity, since they will auto-complete in tools like GraphQL Playground.
In the schema language, an enum looks like this:
enum AllowedColor {
RED
GREEN
BLUE
}
An enum can be used anywhere a scalar can be:
type Query {
favoriteColor: AllowedColor # As a return value
avatar(borderColor: AllowedColor): String # As an argument
}
A query might look like this:
query {
avatar(borderColor: RED)
}
To pass the enum value as a variable, use a string of JSON, like so:
query MyAvatar($color: AllowedColor) {
avatar(borderColor: $color)
}
{
"color": "RED"
}
Putting it all together:
const { ApolloServer, gql } = require('apollo-server');
const typeDefs = gql`
enum AllowedColor {
RED
GREEN
BLUE
}
type Query {
favoriteColor: AllowedColor # As a return value
avatar(borderColor: AllowedColor): String # As an argument
}
`;
const resolvers = {
Query: {
favoriteColor: () => 'RED',
avatar: (parent, args) => {
// args.borderColor is 'RED', 'GREEN', or 'BLUE'
},
}
};
const server = new ApolloServer({ typeDefs, resolvers });
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`)
});
Internal values
Sometimes a backend forces a different value for an enum internally than in the public API. In this example the API contains RED
, however in resolvers we use #f00
instead. The resolvers
argument to ApolloServer
allows the addition of custom values to enums that only exist internally:
const resolvers = {
AllowedColor: {
RED: '#f00',
GREEN: '#0f0',
BLUE: '#00f',
}
};
These don't change the public API at all and the resolvers accept these value instead of the schema value, like so:
const resolvers = {
AllowedColor: {
RED: '#f00',
GREEN: '#0f0',
BLUE: '#00f',
},
Query: {
favoriteColor: () => '#f00',
avatar: (parent, args) => {
// args.borderColor is '#f00', '#0f0', or '#00f'
},
}
};
Most of the time, this feature of enums isn't used unless interoperating with another library that expects its values in a different form.