Learning Logo
Mobile Menu
Copy article linkShare Article on TwitterShare article on LinkedInShare article on FacebookShare article on Pinterest

GraphQL scalars and their input and result coercion

David Mráz
David Mráz@davidm_ai
Author's Linkedin accountAuthor's Instagram accountAuthor's Twitter accountAuthor's Twitter account
Development

Introduction

When designing GraphQL schema we have to define the so-called primitive values of each query. In GraphQL the primitive values are represented by scalars and enums. The GraphQL specification has different built-in scalars. In this first part of this series, we will concentrate only on the built-in scalars. To demonstrate real applications of the built-in scalars we will apply them in a modelling query for retrieving tasks from the existing “in-memory” database and also for adding new tasks to a database. All examples are implemented in the repository.

You can quickly start with:

git clone git@github.com:atherosai/graphql-gateway-apollo-express.git

install dependencies with

npm i

and start the server in development with

npm run dev

You should be now able to access GraphQL Playground.

In GraphQL we deal with 8 different types:

In the article Input object type as an argument for GraphQL mutations and queries we were focusing on objects and input objects. In this series, we are moving to scalars and enums. In this first part, we deal only with built-in scalars; enums and custom scalars are covered in the further articles.

When we query or mutate data using our GraphQL schema, we first have to understand two operations that occur in GraphQL servers. These are:

  • result coercion:  upholding the contract of a type which we receive from the server (basically upholding the primitive values or object type)
  • input coercion:  upholding the contract of a type for input

arguments that we pass into the GraphQL query or mutation To fully understand the algorithm behind the coercion we have to go into the GraphQL specification. For us, the simple explanation as above is enough to get started. It is important to recognize that the rules for each scalar type are different, therefore we have to discuss these rules for each type and corresponding result and input coercion.

GraphQL scalars

Scalars are primitive values in GraphQL. This means that if we imagine each GraphQL response as a hierarchical tree graph, we can call the primitive values leaves in terms of graph theory. To imagine, check out the visualized hierarchy tree graph of our response for our query getTasks and mutation createTask, which we will code in this short tutorial series

In this first part we will deal only with id, name, completed, progress and taskPriority as these are represented by built-in types. In the second part we then extend the Task type with the fields updatedAt, createdAt, and state.

Built-in scalars

In this list, we will go through each default scalar type. We will also discuss rules for result and input coercion and where this scalar should be used.

ID

If you use, for example, some caching client like Apollo, each type should have at least one ID. This allows us to perform a normalization of queries, making it possible for us to update things in Apollo internal redux store automatically based on the unique id (edit: Apollo 2.0+ does not use redux as its store any more). This is also performed in Relay by the global identifier at each node. ID is often numeric or in another format such as base64 encoded value. It is always serialized as a string because we want to achieve uniformity across different formats.

Input coercion

  • 5 is parsed into a string as "5"
  • However, if we pass true, it is not parsed as "true". GraphQL will raise the following error:
Argument "input" has invalid value {id: true}.
In field "id": Expected type "ID", found true

the same case is even for the Float type and etc.

Argument "input" has invalid value {id: 3.00}.
In field "id": Expected type "ID", found 3.00.

Result coercion

ID is serialized into a string if possible:

  • 6 is serialized into “6”
  • true is serialized into “true”
  • 3.00 to “3”

Int

Input coercion

  • string “10” will raise an error when passing it as a Int argument. Also 10.00 will raise an error. Only pure Int values are accepted.
  • The string “3” is serialized into 3 and Float 3.00 is also serialized to 3.

Result coercion

  • The string “3” is serialized into 3 and Float 3.00 is also serialized to 3.

Float

Input coercion

  • String “3” or “3.00” will raise the error in the same way as with Int. When we pass 3 as an integer it is parsed into 3.00.

Result coercion

  • 3 is serialized into 3.00, “3.00” is serialized into 3.00, etc.

String

Input coercion

The rules are similar for ID. The input 5 will raise an error etc.

Result coercion

  • 1 is serialized into "1"
  • 1.00 is serialized into "1", true is serialized into "true" and so on.

Boolean

Input coercion

When we pass "true" as an argument, it raises an error.

Result coercion

If possible, all non-boolean values are coerced into boolean values. The example can be 0 and 0.00 coerced into false as well as 1 and 1.00 serialized into true. However, it is important to emphasize that string "true" or "True" is not coerced into true. GraphQL server will always return true value for every string with the length greater than 0.

An application of built-in scalars

Now let’s go right into implementation. You can follow the code snippets in this article or clone the git repository

git clone git@github.com:atherosai/graphql-gateway-apollo-express.git

If you are new to GraphQL, it may also be helpful to check out previous articles in GraphQL mastery publication, especially the one on creating mutations as we use them as prerequisites for this article in terms of understanding GraphQL. We will also use parts of the code that we built in previous articles. However, everything can be obtained from the mentioned repository. In the GitHub project we do not use a real database. The in-memory database is enough to get started.

To demonstrate each type of built-in scalar, let’s design a Task object type in the following way.

type Task {
id: ID!
name: String!
completed: Boolean!
state: TaskStateEnum!
progress: Float!
taskPriority: Int!
dueDate: DateTime
createdAt: DateTime!
updatedAt: DateTime
}

We will describe DateTime and TaskStateEnum types in our articles In our code we use graphql-js library. Then we can describe Task type as follows.

import {
GraphQLString,
GraphQLID,
GraphQLObjectType,
GraphQLNonNull,
GraphQLInt,
GraphQLFloat,
GraphQLBoolean,
} from 'graphql';
import DateTime from '../custom-scalars/DateTime';
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),
defaultValue: false
},
state: {
type: new GraphQLNonNull(TaskStateEnumType),
},
progress: {
type: new GraphQLNonNull(GraphQLFloat),
},
taskPriority: {
type: new GraphQLNonNull(GraphQLInt),
},
dueDate: {
type: DateTime,
},
createdAt: {
type: new GraphQLNonNull(DateTime),
},
updatedAt: {
type: DateTime,
},
}),
});
export default TaskType

Now let’s create a simple array where we will store our seeded tasks. Then we can make the simple getTasks query and the createTask mutation, which we can call from our resolver function in GraphQL schema. The getTasks query is used to query all the tasks from the in-memory database. The createTask mutation is then used for creating a new task.

import genId from "../lib/gen-id";
import tasks from "../db/tasks-db";
export const createTask = (input) => {
const newTask = {
id: genId(),
...input,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
tasks.unshift(newTask);
return newTask;
};
export const getTaskById = id => tasks.find(task => task.id === id)
export const getTasks = () => tasks;

The corresponding GraphQL schema and resolvers for retrieving tasks from memory DB is as follows:

import {
GraphQLList,
GraphQLNonNull,
} from 'graphql';
import { getTasks } from '../../operations/tasks-operations';
import Task from './TaskType';
const TaskQueries = {
tasks: {
type: new GraphQLNonNull(new GraphQLList(Task)),
resolve: () => getTasks(),
},
};
export default TaskQueries;

Now we have everything we need to perform query for retrieving tasks against the schema. Just go to /graphql and paste the following query

query getTasks {
tasks {
id
name
completed
taskPriority
progress
}
}

If you use the Github project code you should get the following result:

{
"data": {
"tasks": [
{
"id": "7e68efd1",
"name": "Test task",
"completed": false,
"state": "IN_PROGRESS",
"progress": 55.5,
"taskPriority": 1
}
]
}
}

First, let’s create an input object type for creating a task. We will use some defaultValues for some of the fields

import {
GraphQLString,
GraphQLInputObjectType,
GraphQLNonNull,
GraphQLBoolean,
GraphQLInt,
GraphQLFloat,
} from 'graphql';
import TaskStateEnum from './TaskStateEnumType';
import DateTime from '../custom-scalars/DateTime';
const CreateTaskInputType = new GraphQLInputObjectType({
name: 'CreateTaskInput',
fields: () => ({
name: {
type: new GraphQLNonNull(GraphQLString),
},
completed: {
type: GraphQLBoolean,
defaultValue: false,
},
state: {
type: TaskStateEnum,
defaultValue: TaskStateEnum.getValue("ASSIGNED"),
},
taskPriority: {
type: GraphQLInt,
defaultValue: 1,
},
progress: {
type: GraphQLFloat,
defaultValue: 0,
},
dueDate: {
type: DateTime,
},
}),
});
export default CreateTaskInputType;

Now we can move on to designing the createTask mutation

import {
GraphQLNonNull,
} from 'graphql';
import { createTask } from '../../operations/tasks-operations';
import CreateTaskInput from './CreateTaskInputType';
import CreateTaskPayload from "./CreateTaskPayloadType"
const TaskMutations = {
createTask: {
type: CreateTaskPayload,
args: {
input: {
type: new GraphQLNonNull(CreateTaskInput),
},
},
resolve: (_source, { input }) => {
const createdTask = createTask(input);
return {
task: createdTask
}
},
},
};
export default TaskMutations

We can execute this mutation and add a new task into the “in-memory” database. We can also use variables as we discussed in another article or just pass it as an inline argument.

mutation createTask {
createTask(input: {
name: "Next task",
state: ASSIGNED
}) {
task {
id
name
completed
state
progress
taskPriority
}
}
}

The result should be something like this

{
"data": {
"createTask": {
"task": {
"id": "34acac00-0a59-11ea-beea-9918734f89da",
"name": "Next task",
"completed": false,
"state": "ASSIGNED",
"progress": 0,
"taskPriority": 1
}
}
}
}

To illustrate the full example, GraphQL schema in SDL is as follows:

input CreateTaskInput {
name: String!
completed: Boolean = false
state: TaskStateEnum
taskPriority: Int = 1
progress: Float = 0
dueDate: DateTime
}
"""CreateTaskPayload type definition"""
type CreateTaskPayload {
task: Task!
}
"""An ISO-8601 encoded UTC date string."""
scalar DateTime
type Mutation {
createTask(input: CreateTaskInput!): CreateTaskPayload
}
type Query {
tasks: [Task]!
}
type Task {
id: ID!
name: String!
completed: Boolean!
state: TaskStateEnum!
progress: Float!
taskPriority: Int!
dueDate: DateTime
createdAt: DateTime!
updatedAt: DateTime
}
enum TaskStateEnum {
ASSIGNED
UNASSIGNED
IN_PROGRESS
}

Input & result coercion of demo examples

For the sake of demonstration, we can now test some rules for result or input coercion. For example, let’s take a result coercion for ID and test the rule that an integer is serialized into a string.

We can change the value of id to 6 in task-db in order to test such task. We can confirm the result coercion rule by calling the getTasks query in GraphQL playground in the same way as above:

{
"data": {
"tasks": [
{
"id": "6",
"progress": 55.5,
"name": "Test task",
"completed": false,
"taskPriority": 1
}
]
}
}

In a similar way we can confirm some rule for input coercion. Let’s take for example rule for Int.

We can show this rule on the createTask mutation,

mutation createTask {
createTask(input: {name: "New task", taskPriority: "10"}) {
id
taskPriority
progress
completed
}
}

which raises the following error

{
"errors": [
{
"message": "Argument "input" has invalid value {name: "New task", taskPriority: "10"}.
In field "taskPriority": Expected type "Int", found "10".",
"locations": [
{
"line": 2,
"column": 18
}
]
}
]
}

Conclusion

All built-in scalars can be used as fields (primitive values) for input types as well as for output types. Each built-in GraphQL scalar has different rules for input and result coercion. We can also use enum and custom scalars as we will find out in the next part of this series.

Ready to take next step?

Unlock your potential and master the art of development and design by joining our Classes today.

Don't miss out on the opportunity to enhance your skills and create a bright future in the digital world like thousands of others.