GraphQL Scalars in-depth
January 16, 2020
This is an in depth explanation of how GraphQL Scalars work and how to implement custom Scalars. The code examples are implemented in the reference implementation GraphQL.js. The general GraphQL concepts apply to every language/ecosystem and are not limited to JS.
Overview
Scalars are one of two “atomic” types of the GraphQL type system. Enums are the other one, but because there are no custom Enums they are fundamentally simpler and we will not cover them here.
Atomic means that if you follow every field and every argument declaration in GraphQL you will encounter a Scalar or Enum at the end.
GraphQL defines 5 built-in Scalars:
- Int: a signed 32‐bit numeric non‐fractional value
- Float: signed double‐precision fractional values as specified by IEEE 754
- String: textual data, represented as UTF‐8 character sequences
- Boolean: true of false
- ID: a unique identifier
On top of these built-in Scalars every GraphQL server implementation can offer additional ones and every server implementation allows user to define their own custom Scalars.
Coercion
The fundamental job of a Scalar is to give type safety about the values which are returned or which can be used as input.
This type safety is implemented through three functions per Scalar. Each function is invoked at a specific time by the GraphQL engine. They all validate and convert a provided value. This conversion is called “Coercion” in GraphQL.
Overview of the coercion functions:
We will look at all three coercion functions.
Result Coercion
Result Coercion takes the result of a resolver and converts it into an appropriate value for the result.
The details depend on the implementation, but the general idea is to accept values which can be clearly and in a predictable way converted to the expected type.
For example GraphQL.js for the String
Scalar accepts JS string, boolean and numbers values and JS objects which can be converted to string, boolean or number.
GraphQL Java accepts all Objects for String
because Java has a built-in function to convert any Object to String.
On the other hand Int
is a much more restricted type.
Therefore GraphQL.js does only accept numbers and strings representing numbers.
Literal Input Coercion
Literal Input Coercion takes an abstract syntax tree (Ast) element from a schema definition or query and converts it into an appropriate argument value for a resolver.
Literal Input Coercion happens in the following places:
Query field arguments:
{ foo(stringArg: "String", intArg: 1234) }
Query directive arguments:
{ foo @skip(if: true) }
Schema directive arguments:
type Query {
hello :String @myDirective(arg: "124")
}
Schema default values:
directive @myDirective(arg: String = "defaultValue") on FIELD
input MyInput {
foo: String = "bar"
}
type Query {
hello(arg: Int = 123): String
}
The details of the Literal Input Coercion are much less platform dependent compared to Result Coercion because the input is clearly defined.
The GraphQL spec defines what literal values are accepted for each type.
For example Int
only accepts Int literals, String
only String literals. But ID
accepts Int and String literals:
# example of ID accepting String and Int literals as input
type Query {
hello(arg1: ID = "123", arg2: ID = 123): String
}
Value Input Coercion
Argument values can not only provided via GraphQL syntax inside a query, but also via variable:
query MyQuery($var: String) {
foo(arg: $var)
}
In case of input object arguments you can also use variables for parts of the input object:
query MyQuery($var: String) {
foo(arg: {someValue: "hello", othValue: $var})
}
In order to execute this query the engine needs to have a value for $var
.
In GraphQL.js you provide a map with variable values by name when you execute a query:
const variableValues = {var: "SomeString"};
const query = `
query MyQuery($var: String) {
foo(arg: $var)
}
`;
const result = graphql(query, null, null, variablesValues);
The details are again very much implementation specific but the idea is the same as for Result Coercion: the Value Input Coercion function should accept values which can be converted easily and predictable into the type of the Scalar.
For example in GraphQL.js Int
only accepts numbers which are integers as argument.
GraphQL Java is more lenient in the Int
implementation and accepts also Strings which can be converted to an Integer.
Serialization vs Coercion
How a GraphQL response and variable input values are serialized is nominally separated from the Coercion described above.
But JSON is by far the most used serialization format and the only one that is clearly defined in the specification.
If only GraphQL built-in Scalars are used a GraphQL engine will normally produce a result which can be serialized into JSON without any extra effort.
For example GraphQL.js returns a JS Object which can be serialized via JSON.stringify
and GraphQL Java returns a Map
which directly represents a JSON document.
Implementing a custom Scalar
In GraphQL.js the functions corresponding to the coercion described above are named serialize
, parseLiteral
and parseValue
. (See this issue about the naming.)
serialize
= “Result Coercion”
parseLiteral
= “Literal Input Coercion”
parseValue
= “Value Input Coercion”
Creating a custom Scalar involves implementing these methods:
const myCustomScalarType = new GraphQLScalarType({
name: 'MyCustomScalar',
// result coercion
serialize(value) {
//TODO: implement and return value
},
// literal input coercion
parseLiteral(ast) {
//TODO: implement and return value
},
// value input coercion
parseValue(value) {
//TODO: implement and return value
}
});
If the schema is built via SDL and not programmatically a custom Scalar needs to be declared:
scalar MyCustomScalar
It is still required to provide a GraphQLScalarType
when building the full schema.
Implementation considerations
Result Coercion
Result Coercion takes a resolved value (the value returned by a resolver) as a parameter, therefore the accepted values should be all values which makes sense to represent the custom Scalar. This can be different types like a complex object or a string. A Date Scalar in JS for example could accept a String formatted according to RFC 3339 and a JS Date object as a parameter.
The returned value of a the result coercion is part of the overall execution result. Normally this value is a primitive value like String or Integer to make it easy for the serialization layer. For complex types like a Date or Money Scalar this involves formatting the value.
Input Coercion
The Input Coercion results will be consumed by resolvers as arguments:
const fooResolver = (root, {arg} ) => {
console.log(`ast input coercion result: ${arg}`);
};
The resolver doesn’t know and doesn’t care if the value comes from a variable (Input Value Coercion) or from an ast literal (Literal Input Coercion):
# literal input
{ foo(arg: "myValue") }
# value input
query MyQuery($var: String){ foo(arg: $var) }
This means the Literal Input and the Value Input Coercion should return the same values for semantically equal arguments. For example it should not matter if you provide a String literal for a Date Scalar or you provide a JS Date object via variable reference: the value passed to the resolver should be the same.
Because these values are consumed by code, Input Coercion should not only return primitive values like String but rather complex ones when appropriate.
For example a Money Scalar should probably provide not a string or number but rather an Object with multiple properties.
Example Money Scalar
We will build an simplified Money Scalar which only deals with EUR.
The Scalar introduces a class Money
to represent euro values separated in full euro
and in cents. (This is just example code which ignores a lot of details around currencies.)
class Money {
constructor(euro, cents) {
this.euro = euro;
this.cents = cents;
}
toString() {
return this.euro + "." + this.cents;
}
}
Money Result Coercion
We want to make it as convenient as possible for the user to provide money values, therefore we support string, number and Money objects.
The money is always formatted via Money.toString()
:
// result coercion
// return formatted Money string
serialize(value) {
if (typeof value === 'string') {
const money = parseMoneyString(value);
return money.toString();
}
if (typeof value === 'number') {
const money = parseCentsNumber(value);
return money.toString();
}
if (value instanceof Money) {
return value.toString();
}
throw new GraphQLError(
`Invalid value from resolver for Money ${value}`,
);
},
Money Literal Input Coercion
For the Literal Value Coercion we want to support String
literals (e.g. “49.99”) and also Int
literals representing cents.
As mentioned above both Input Coercion functions should return the same values, in our example this means we are returning also Money objects.
// literal input coercion
// returns Money object
parseLiteral(ast) {
if (ast.kind === Kind.STRING) {
return parseMoneyString(ast.value);
}
if (ast.kind == Kind.INT) {
const cents = parseInt(ast.value);
return parseCentsNumber(cents);
}
throw new GraphQLError(
`Invalid literal for money ${print(ast)}`,
);
}
Money Value Input Coercion
The code for the input value coercion is nearly identical to the result coercion, but the return value is different: we want to return the Money object itself to make it easy for the resolver code which consumes it.
// value input coercion
// returns Money object
parseValue(value) {
if (typeof value === 'string') {
return parseMoneyString(value);
}
if (typeof value === 'number') {
return parseCentsNumber(value);
}
if (value instanceof Money) {
return value;
}
throw new GraphQLError(
`Invalid input value for Money ${value}`,
);
Full example source code
The full source code for the Money example can be found here.
Custom Scalar specification URL
(Note: as of January 2020 this sections covers an unreleased feature of Graphql, therefore details might still change before it is released.)
One fundamental problem with custom Scalars is that there is no good way to communicate their Coercion rules: what values are allowed as literals? String? Int? Boolean? What format can I expect to be returned?
So far the only option is to add a comprehensive description and hope the users will read them. But tools like GraphiQL (which could offer code completion for example) are out of luck.
GraphQL will soon address this problem by adding a new feature: Custom Scalar Specification URL.
This feature introduces a new property specifiedBy
which holds a URL documenting a custom Scalar:
const myCustomScalarType = new GraphQLScalarType({
name: 'MyCustomScalar',
specifiedBy: 'https://myCustomScalar.com/spec'
...
});
In SDL it is declared with a special directive:
scalar MyCustomScalar @specifiedBy(url: "https://myCustomScalar.com/spec")
The URL should document the Scalar and it also serves as unique identifier.
This will allow clients to build specific behavior around different specification URLs and make documenting especially for complex Scalars easier.
Further resources and Feedback
The GraphQL specification describes Scalars in section 3.5.
GraphQL.js implements Scalars in scalars.js.
Graphql.org has a short introduction into Scalars
Thanks for reading.
Please reach out to me via twitter @andimarek for any feedback.
Written by Andi Marek You should follow him on Twitter