September 1st 2018 (3 months ago)

GraphQL List - How to use arrays in GraphQL schema (GraphQL Modifiers)

Author profile picture
David Mráz (@david_mraz1)

Introduction

It is often common practice in REST APIs to return a JSON response with an array of objects. In GraphQL we would like to follow this pattern as well. In this article we will go through modifiers, a special group of types which allows us to modify the default behavior of other types. In GraphQL we deal with various groups of types. These groups are as follows:

It may be helpful first to go through the articles above. After gaining a fundamental understanding of other types such as scalars and object types you can then move on to modifiers. Next we can start working on the project setup so that we can test our queries. We assume that yarn, git and Node.js versions higher than 8 are already installed on your computer. Now you can execute this command in your shell

git clone https://github.com/a7v8x/express-graphql-demo.git -b feature/5-modifiers

and install dependencies with

yarn install

After that you should be able to run the GraphQL server with

yarn run dev

Then you can move to http://localhost:3000/graphql to execute the queries available in this article. In the model project, we use the “in-memory” database with fake data for executing our queries.

Model schema

Let’s first consider this model schema, which was printed with the printSchema function from graphql-js utilities. The model schema in the repository is built with a class-based approach using the graphql-js library. It is often much clearer to view the whole schema written in Schema definition language (SDL). For some time now, SDL has been a part of the specification and it is often used to build the schema itself using the buildschema utility or the library called graphql-tools

input CreateUserInput {
  username: String!
  email: String
  phone: String
  firstName: String
  lastName: String
}
type Mutation {
  createUsers(input: [CreateUserInput]): [User]
}
type Query {
  user(id: ID!): User
  users(role: RoleEnum): [User!]!
}
enum RoleEnum {
  admin
  accountant
}
type User {
  id: ID!
  username: String!
  email: String
  phone: String
  firstName: String
  lastName: String
}

We can see that we have defined one output object type called User with the following fields: id, username, email, phone, firstname, lastname. The id field is typed as an ID scalar and other fields are typed as Strings. We’ve also defined the queries user and users. The user query returns the User object based on the passed id. The users query then returns a list of users. We have also defined the nonrequired enum type role, which is used in the users query as an argument for filtering the result. In this simple schema we used modifiers quite a lot. In the rest of the article we will go through these use cases.

Modifiers

First let’s formally define modifier. As we have already mentioned, modifier is a special group of types in GraphQL. These types can be defined as follows:

A Modifier modifies the type to which it refers.

From this definition it is clear that we always need to define the type to which we are applying the modifier. In current GraphQLspecification, we have these two types of modifiers. Each of the modifier is classified as a separate type:

  • List
  • NonNull

The list modifier will be our main focus in this article. It will allow us to define if we would like to return a sequence of types. A non-null modifier allows us to define if the type/field is required. This can be null (default behavior in GraphQL) or is required and the GraphQL server raises an error. In this article we will focus mainly on List modifiers and leave a more in-depth discussion of non-null modifiers for another article.

List

In general, a GraphQL list represents a sequence of values. It is possible to view these values as arrays (e.g. in Javascript), although the analogy is not completely precise. As we mentioned a list keeps items in order. In SDL the list modifier is written as square brackets with the wrapped instance of the type in the bracket. In our schema we used the list modifier to define that if we call the query users, it returns a sequence of types of User from the database. This is achieved by defining the schema as follows:

type Query {
  user(id: ID!): User
  users(role: RoleEnum): [User!]!
}

By calling query users we expect to return a list of users. Let’s see how this looks when we use the graphql-js library. The queries in our repository are defined as follows:

import {
  GraphQLList,
  GraphQLID,
  GraphQLNonNull,
} from 'graphql';
import { getUsers, getOneUser } from '../../db/usersDb';
import User from './userType';
import UserRoleEnum from './userRoleEnumType';

const userQueries = {
  user: {
    type: User,
    args: {
      id: {
        type: new GraphQLNonNull(GraphQLID),
      },
    },
    resolve: async (source, { id }) => {
      const result = await getOneUser({ id });
      return result;
    },
  },
  users: {
    type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(User))),
    args: {
      role: {
        type: UserRoleEnum,
      },
    },
    resolve: async (source, { role }) => {
      const result = await getUsers();
      if (role) {
        return result.filter(user => user.role === role);
      }
      return result;
    },
  },
};

export {
  userQueries as default,
};

We can see that we achieve the same functionality as with SDL. The GraphQLList class represents the List in graphql-js. We have applied the instance of this class to the instance of User. Now we are able to fetch the data by executing the users query in GraphQL playground with the “Play” button.

We should retrieve this data and obtain users as a list.

{
  "data": {
    "users": [
      {
        "id": "7b838108-3720-4c50-9de3-a7cc04af24f5",
        "firstName": "Berniece",
        "lastName": "Kris",
        "username": "Ana_Quigley"
      },
      {
        "id": "66c9b0fd-7df6-4e2a-80c2-0e4f8cdd89b1",
        "firstName": "Bradly",
        "lastName": "Lind",
        "username": "Winona_Kulas12"
      },
      {
        "id": "718590a1-33ac-4e61-9fef-b06916acd76b",
        "firstName": "Leila",
        "lastName": "Schowalter",
        "username": "Isabell.Kautzer"
      },
      {
        "id": "411df0f3-bb2c-4f5f-870f-3db9c30d754f",
        "firstName": "Laila",
        "lastName": "Breitenberg",
        "username": "Christophe.Oberbrunner"
      },
      {
        "id": "e1254480-d205-4be8-abfa-67ad7dcd03fb",
        "firstName": "Joe",
        "lastName": "Crist",
        "username": "Dahlia.Gerhold56"
      },
      {
        "id": "d0087200-9621-4787-a3db-cebbede163e6",
        "firstName": "Bettye",
        "lastName": "Bartoletti",
        "username": "Thad_Mayert"
      }
    ]
  }
}

The other use case for List modifiers is for designing the createUsers mutation, where we can add users in batch. There are multiple reasons to design the mutations in this way. We may need to add users in transaction, therefore we cannot have a different resolver context or we just want to simplify the API or improve the performance and execute the mutation for multiple users more quickly. This is a great use case for applying the List modifier to our input payload. We can define the input object type just once like this:

import {
  GraphQLString,
  GraphQLInputObjectType,
  GraphQLNonNull,
} from 'graphql';

const createUserInputType = new GraphQLInputObjectType({
  name: 'CreateUserInput',
  fields: () => ({
    username: {
      type: new GraphQLNonNull(GraphQLString),
    },
    email: {
      type: GraphQLString,
    },
    phone: {
      type: GraphQLString,
    },
    firstName: {
      type: GraphQLString,
    },
    lastName: {
      type: GraphQLString,
    },
  }),
});

export {
  createUserInputType as default,
};

or in SDL language

input CreateUserInput {
  username: String!
  email: String
  phone: String
  firstName: String
  lastName: String
}

and then apply list modifier to achieve the ability of passing multiple payloads in one input variable.

import { GraphQLList } from 'graphql';
import { createUser } from '../../db/usersDb';
import CreateUserInput from './createUserInputType';
import User from './userType';

const userMutations = {
  createUsers: {
    type: new GraphQLList(User),
    args: {
      input: {
        type: new GraphQLList(CreateUserInput),
      },
    },
    resolve: async (source, { input }) => {
      const result = input.map(userPayload => createUser(userPayload));
      return result;
    },
  },
};

export { userMutations as default };

We can execute the mutation with using inline arguments or if you prefer with using variables

mutation {
  createUsers(input: [{lastName: "Test", firstName: "test", username: "t1est"}, {lastName: "Test", firstName: "test", username: "te2st"}]) {
    username
    lastName
    firstName
  }
}

Now let’s go through the rules for result and input coercion. If you are not familiar with these terms, you can take a look at the article on scalars, where we describe input and result coercion.

Result coercion

For the query users ,result coercion is relevant for us as we would like to obtain an array of users from the executed query. When we coerce lists, the GraphQL server needs to ensure that the returned data from the resolver function will remain in the same order. The coercion of the each item in the list is then delegated to the result coercion of the referenced type; each item of the array needs to comply to User type or null value. If it returns an object instead of array like in this resolver function:

users: {
   type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(User))),
   args: {
     role: {
       type: UserRoleEnum,
     },
   },
   resolve: async (source, { role }) => {
     const result = await getUsers();
     if (role) {
       return result.filter(user => user.role === role);
     }
     return {};
   },
 },

the GraphQL server should then raise this error

Expected Iterable, but did not find one for field Query.users.
This happens if the coercion of the List modifier does not comply But what happens if some of the items in the list do not coerce properly? In that case we handle the error in a similar manner. We return nullinstead of the value returned from the resolver function and add an error to the response.

Input coercion

When discussing input coercion of List modifiers we can take into account the createUsers mutation and describe the behavior that raises an error. In contrast to the result coercion, where some items from the result array can be obtained even if one item is not coerced properly, in input coercion we will not be able to execute the whole mutation if one payload cannot be coerced. Let’s take a look at the following example, where we would like to pass a list of two payloads, but one payload does not comply to the input type and does not have the requiredusername field. Upon executing this mutation we receive the following error:
Argument "input" has invalid value [{username: "testtest", email: "test@test.com", firstName: "test", lastName: "test"}, {email: "test@test.com", firstName: "test", lastName: "test"}].
In element #1: In field "username": Expected "String!", found null.
The whole mutation fails even if only the input coercion in the input object type in one item in the list does not comply. However, it is important to emphasize that if we pass null as follows, the whole mutation will be executed. However, this depends on whether or not we applied any additional modifiers and composed the modifiers in a more complex type. We will go through this topic in the last section of this article on Modifier composition

Modifier composition

If we consider the definition of the modifier above, we know that the modifier basically creates a new type from the referenced type with additional functionality. In our case we are adding behavior so that the result coercion will accept a list of items and not just the item itself. This is also similar to higher order functions or the decorator pattern and in the same manner we can chain higher order functions or HOCs in React. We are also able to compose modifiers by applying a modifier to the type where the previous modifier is already applied. We can combine the Non-Null modifier with our List modifier in the following way. This way we basically combine three modifiers, which are chained as follows

new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(User)))

This creates a special type. When using only a list modifier we are allowed to return a null value from the resolver. We can even combine the items in the array to contain null values as in this array:

mutation {
  createUsers(input: [{username: "testtest", email: "test@test.com", firstName: "test", lastName: "test"}, null]) {
    id
    username
    firstName
  }
}

But when we apply the composed modifier as above, we are only allowed to pass the array containing the objects that comply to the User type. The list above will therefore be rejected. The null value returned from resolver will be also rejected. You can take a look at the table below, which contains what each modifier will allow in order to get a better idea of which combinations of modifiers are suitable for different use cases. The only rule in chaining modifiers applies to Non-null modifiers. It declares that we cannot wrap one Non-Null modifier with another Non-Null modifier.

ModifierTestResult
[User][UserObject, null]Valid
[User]nullValid
[User][null]Valid
[User][UserObject]Valid
[User!][UserObject,null]Invalid
[User!][null]Invalid
[User!]nullValid
[User!][UserObject]Valid
[User!]![UserObject, null]Invalid
[User!]!nullInvalid
[User!]![UserObject]Valid
User!!-Invalid
UserObject in this table can be equal for example to
{ lastName: "Test", firstName: "test", username: "t1est"}
For simplicity, i did not cover differences between input and output coercion for these more complex types. The behaviour is different only as we discussed in the result and input coercion section. If there would be different UserObject, which does not comply to User type coercion (e.g. does not have username property), there would be additional rules.

Summary

In this article we have covered one special group of types in GraphQL called Modifiers. With modifiers we are allowed to inject special behavior into the referenced GraphQL type, add a List and other required fields, and even combine these use cases to build more complex types. Modifiers are a great tool to make elegant GraphQL schemas.

Feel free to send any questions about this article or GraphQL Mastery in general to david@atheros.ai. You can also check out our upcoming free GraphQL language course or subscribe for new articles on GraphQL Mastery with the form below. If you prefer you can also checkout republished medium version of the article.

Join our newsletter to get notified about new articles and get instant access to our upcoming free GraphQL Course

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