Understanding the GraphQL Type System
Introduction
GraphQL is a modern solution for facilitating the communication between a front end and a data source. All of the details and capabilities of a GraphQL implementation are laid out in the GraphQL Schema. In order to write a functioning GraphQL schema, you must understand the GraphQL Type System.
In this article, you will learn about GraphQL types: the five built-in scalar types, Enums, the List and Non-Null wrapping types, Object types, and the abstract Interface and Union types that work alongside them. You will review examples for each type and learn how to use them to build a complete GraphQL schema.
Prerequisites
To get the most out of this tutorial, you should have:
- An understanding of the fundamental concepts of GraphQL, which are laid out in An Introduction to GraphQL.
- A GraphQL environment, an example of which can be found in How to Set Up a GraphQL API Server in Node.js.
Scalar Types
All the data in a GraphQL schema ultimately resolve to various scalar types, which represent primitive values. GraphQL responses can be represented as a tree, and the scalar types are the leaves at the ends of the tree. There can be many levels in a nested response, but the last level will always resolve to a scalar (or Enum) type. GraphQL comes with five built-in scalar types: Int
, Float
, String
, Boolean
, and ID
.
Int
Int
is a signed 32-bit non-fractional numerical value. It is a signed (positive or negative) integer that does not include decimals. The maximum value of a signed 32-bit integer is 2,147,483,647
. This is one of the two built-in scalars used for numerical data.
Float
A Float
is a signed double-precision fractional value. It is a signed (positive or negative) number that contains a decimal point, such as 1.2
. This is the other built-in scalar used for numerical data.
String
A String
is a UTF-8 character sequence. The String
type is used for any textual data. This can also include data like very large numbers. Most custom scalars will be types of string data.
Boolean
A Boolean
is a true
or false
value.
ID
An ID
is a unique identifier. This value is always serialized as a string, even if the ID
is numerical. An ID
type might be commonly represented with a Universally Unique Identifier (UUID).
Custom Scalars
In addition to these built-in scalars, the scalar
keyword can be used to define a custom scalar. You can use custom scalars to create types that have additional server-level validation, such as Date
, Time
, or Url
. Here is an example defining a new Date
type:
scalar Date
The server will know how to handle interactions with this new type using the GraphQLScalarType
.
Enum Type
The Enum type, also known as an Enumerator type, describes a set of possible values.
Using the Fantasy Game API theme from other tutorials in the series, you might make an enum
for the game characters' Job
and Species
with all the values the system will accept for them. An Enum is defined with the enum
keyword, like so:
"The job class of the character."
enum Job {
FIGHTER
WIZARD
}
"The species or ancestry of the character."
enum Species {
HUMAN
ELF
DWARF
}
In this way, it is guaranteed that the Job
of a character is FIGHTER
or WIZARD
and can never accidentally be "purple"
or some other random string, which could be possible if you used a String
type instead of making a custom Enum. Enums are written in all-caps by convention.
Enums can also be used as the accepted values in arguments. For example, you might make a Hand
enum
to denote whether a weapon is single-handed (like a short sword) or double-handed (like a heavy axe), and use that to determine whether one or two can be equipped:
enum Hand {
SINGLE
DOUBLE
}
"A valiant weapon wielded by a fighter."
type Weapon {
name: String!
attack: Int
range: Int
hand: Hand
}
type Query {
weapons(hand: Hand = SINGLE): [Weapon]
}
The Hand
enum
has been declared with SINGLE
and DOUBLE
as values, and the argument on the weapons
field has a default value of SINGLE
, meaning if no argument is passed then it will fall back to SINGLE
.
Non-Null Type
You might notice that null
or undefined
, a common type that many languages consider a primitive, is missing from the list of built-in scalars. Null does exist in GraphQL and represents the lack of a value.
All types in GraphQL are nullable by default and therefore null
is a valid response for any type. In order to make a value required, it must be converted to a GraphQL Non-Null type with a trailing exclamation point. Non-Null is defined as a type modifier, which are types used to modify the type it is referring to. As an example, String
is an optional (or nullable) string, and String!
is a required (or Non-Null) string.
List Type
A List type in GraphQL is another type modifier. Any type that is wrapped in square brackets ([]
) becomes a List type, which is a collection that defines the type of each item in a list.
As an example, a type defined as [Int]
will be a collection of Int
types, and [String]
will be a collection of String
types. Non-Null and List can be used together to make a type both required and defined as a List, such as [String]!
.
Object Type
If GraphQL scalar types describe the "leaves" at the end of the hierarchical GraphQL response, then Object types describe the intermediary "branches", and almost everything in a GraphQL schema is a type of Object.
Objects consist of a list of named fields (keys) and the value type that each field will resolve to. Objects are defined with the type
keyword. At least one or more fields must be defined, and fields cannot begin with two underscores (__
) to avoid conflict with the GraphQL introspection system.
In the GraphQL Fantasy Game API example, you could create a Fighter
Object to represent a type of character in a game:
"A hero with direct combat ability and strength."
type Fighter {
id: ID!
name: String!
level: Int
active: Boolean!
}
In this example, the Fighter
Object type has been declared, and it has has four named fields:
id
yields a Non-NullID
type.name
yields a Non-NullString
type.level
yields anInt
type.active
yields a Non-NullBoolean
type.
Above the declaration, you can also add a comment using double quotes, as in this example: "A hero with direct combat ability and strength."
. This will appear as the description for the type.
In this example, each field resolves to a scalar type, but Object fields can also resolve to other Object types. For example, you could create a Weapon
type, and the GraphQL schema can be set up where the weapon
field on the Fighter
will resolve to a Weapon
Object:
"A valiant weapon wielded by a fighter."
type Weapon {
name: String!
attack: Int
range: Int
}
"A hero with direct combat ability and strength."
type Fighter {
id: ID!
name: String!
level: Int
active: Boolean!
weapon: Weapon
}
Objects can also be nested into the fields of other Objects.
Root Operation Types
There are three special Objects that serve as entrypoints into a GraphQL schema: Query, Mutation, and Subscription. These are known as Root Operation types and follow all the same rules as any other Object type.
The schema
keyword represents the entrypoint into a GraphQL schema. Your root Query, Mutation, and Subcription types will be on the root schema
Object:
schema {
query: Query
mutation: Mutation
subscription: Subscription
}
The Query type is required on any GraphQL schema and represents a read request, similar to a REST API GET
. The following is an example of a root Query
Object that returns a List of Fighter
types:
type Query {
fighters: [Fighter]
}
Mutations represent a write request, which would be analogous to a POST
, PUT
, or DELETE
in a REST API. In the following example, the Mutation
has an addFighter
field with a named argument (input
):
type Mutation {
addFighter(input: FighterInput): Fighter
}
Finally, a Subscription corresponds to an event stream, which would be used in conjunction with a Websocket in a web app. In the GraphQL Fantasy API, perhaps it could be used for random battle encounters, like so:
type Subscription {
randomBattle(enemy: Enemy): BattleResult
}
Note that the schema
entrypoint is often abstracted away in some GraphQL implementations.
Field Arguments
The fields of a GraphQL Object are essentially functions that return a value, and they can accept arguments like any function. Field arguments are defined by the name of the argument followed by the type. Arguments can be any non-Object type. In this example, the Fighter
Object can be filtered by the id
field (which resolves to a Non-Null ID
type):
type Query {
fighter(id: ID!): Fighter
}
This particular example is useful for fetching a single item from the data store, but arguments can also be used for filtering, pagination, and other more specific queries.
Interface Type
Like the Object type, the abstract Interface type consists of a list of named fields and their associated value types. Interfaces look like and follow all the same rules as Objects but are used to define a subset of an Object's implementation.
So far in your schema, you have a Fighter
Object, but you might also want to make a Wizard
, a Healer
, and other Objects that will share many of the same fields but have a few differences. In this case, you can use an Interface to define the fields they all have in common, and create Objects that are implementations of the Interface.
In the following example, you could create a BaseCharacter
Interface using the interface
keyword with all the fields every type of character will possess:
"A hero on a quest."
interface BaseCharacter {
id: ID!
name: String!
level: Int!
species: Species
job: Job
}
Every character type will have the fields id
, name
, level
, species
, and job
.
Now, imagine you have a Fighter
type and a Wizard
type that have these shared fields, but Fighters
use a Weapon
and Wizards
use Spells
. You can use the implements
keyword to delineate each as a BaseCharacter
implementation, which means they must have all the fields from the created Interface:
"A hero with direct combat ability and strength."
type Fighter implements BaseCharacter {
id: ID!
name: String!
level: Int!
species: Species
job: Job!
weapon: Weapon
}
"A hero with a variety of magical powers."
type Wizard implements BaseCharacter {
id: ID!
name: String!
level: Int!
species: Species
job: Job!
spells: [Spell]
}
Fighter
and Wizard
are both valid implementations of the BaseCharacter
Interface because they have the required subset of fields.
Union Type
Another abstract type that can be used with Objects is the Union type. Using the union
keyword, you can define a type with a list of Objects that are all valid as responses.
Using the Interfaces created in the previous section, you can create a Character
Union that defines a character as a Wizard
OR a Fighter
:
union Character = Wizard | Fighter
The equal character (=
) sets the definition, and the pipe character (|
) functions as the OR
statement. Note that a Union must consist of Objects or Interfaces. Scalar types are not valid on a Union.
Now if you query for a list of characters, it could use the Character
Union and return all Wizard
and Fighter
types.
Conclusion
In this tutorial, you learned about many of the types that define the GraphQL type system. The most fundamental types are the scalar types, which are the values that act as the leaves on the schema tree, and consist of Int
, Float
, String
, Boolean
, ID
, and any custom scalar that a GraphQL implementation decides to create. Enums are lists of valid constant values that can be used when you need more control over a response than simply declaring it as a String
, and are also leaves on the schema tree. List and Non-Null types are known as type modifiers, or wrapping types, and they can define other types as collections or required, respectively. Objects are the branches of the schema tree, and almost everything in a GraphQL schema is a type of Object, including the query
, mutation
, and subscription
entrypoints. Interface and Union types are abstract types that can be helpful in defining Objects.
For further learning, you can practice creating and modifying a GraphQL schema by reading the How to Set Up a GraphQL API Server in Node.js tutorial to have a working GraphQL server environment.
Comments