Schemas and Types
Learn about the different elements of the GraphQL type system
The GraphQL type system describes what data can be queried from the API. The collection of those capabilities is referred to as the serviceâs schema and clients can use that schema to send queries to the API that return predictable results.
On this page, weâll explore GraphQLâs six kinds of named type definitions as well as other features of the type system to learn how they may be used to describe your data and the relationships between them. Since GraphQL can be used with any backend framework or programming language, weâll avoid implementation-specific details and talk only about the concepts.
Type system
If youâve seen a GraphQL query before, you know that the GraphQL query language is basically about selecting fields on objects. So, for example, in the following query:
- We start with a special ârootâ object
- We select the
hero
field on that - For the object returned by
hero
, we select thename
andappearsIn
fields
Because the shape of a GraphQL query closely matches the result, we can predict what the query will return without knowing that much about the server. But itâs useful to have an exact description of the data we can request. For example, what fields can we select? What kinds of objects might they return? What fields are available on those sub-objects?
Thatâs where the schema comes in. Every GraphQL service defines a set of types that completely describe the set of possible data we can query on that service. Then, when requests come in, they are validated and executed against that schema.
Type language
GraphQL services can be written in any language and there are many different approaches you can take when defining the types in a schema:
- Some libraries have you construct the schema types, fields, and resolver functions together using the same programming language that was used to write the GraphQL implementation.
- Some libraries allow you to define types and fields more ergonomically using whatâs commonly called the schema definition language (or SDL) and then write the resolver functions for the corresponding fields separately.
- Some libraries allow you to write and annotate the resolver functions, and then infer the schema from that.
- Some libraries may even infer both the types and resolver functions for you, based on some underlying data source(s).
Since we canât rely on a specific programming language to discuss GraphQL schemas in this guide, weâll use SDL because itâs similar to the query language that weâve seen so far and allows us to talk about GraphQL schemas in a language-agnostic way.
Object types and fields
The most basic components of a GraphQL schema are Object types, which just represent a kind of object you can fetch from your service, and what fields it has. In SDL, we represent it like this:
type Character {
name: String!
appearsIn: [Episode!]!
}
The language is readable, but letâs go over it so that we can have a shared vocabulary:
Character
is a GraphQL Object type, meaning itâs a type with some fields. Most of the types in your schema will be Object types.name
andappearsIn
are fields on theCharacter
type. That means thatname
andappearsIn
are the only fields that can appear in any part of a GraphQL query that operates on theCharacter
type.String
is one of the built-in Scalar types. These are types that resolve to a single scalar value and canât have sub-selections in the query. Weâll go over Scalar types more later.String!
means that the field is a Non-Null type, meaning the GraphQL service promises to give you a value whenever you query this field. In SDL, we represent those with an exclamation mark.[Episode!]!
represents an List type ofEpisode
objects. When a List is Non-Null, you can always expect an array (with zero or more items) when you query theappearsIn
field. In this case, sinceEpisode!
is also Non-Null within the list, you can always expect every item in the array to be anEpisode
object.
Now you know what a GraphQL Object type looks like and how to read the basics of SDL.
Arguments
Every field on a GraphQL Object type can have zero or more
arguments, for example,
the length
field below:
type Starship {
id: ID!
name: String!
length(unit: LengthUnit = METER): Float
}
All arguments are named. Unlike languages such as JavaScript and Python where
functions take a list of ordered arguments, all arguments in GraphQL are passed
by name specifically. In this case, the length
field has one defined argument
called unit
.
Arguments can be either required or optional. When an argument is optional, we
can define a default value. If the unit
argument is not passed, then it will
be set to METER
by default.
The Query, Mutation, and Subscription types
Every GraphQL schema must support query
operations. The entry point for this
root operation type
is a regular Object type called Query
by default. So if you see a query that
looks like this:
That means that the GraphQL service needs to have a Query
type with a droid
field:
type Query {
droid(id: ID!): Droid
}
Schemas may also support mutation
and subscription
operations by adding
additional Mutation
and Subscription
types and then defining fields on the
corresponding root operation types.
Itâs important to remember that other than the special status of being entry
points into the schema, the Query
, Mutation
, and Subscription
types are
the same as any other GraphQL Object type, and their fields work exactly the
same way.
You can name your root operation types differently too; if you choose to do so
then you will need to inform GraphQL of the new names using the schema
keyword:
schema {
query: MyQueryType
mutation: MyMutationType
subscription: MySubscriptionType
}
Scalar types
A GraphQL Object type has a name and fields, but at some point, those fields must resolve to some concrete data. Thatâs where the Scalar types come in: they represent the leaf values of the query.
In the following query, the name
and appearsIn
fields will resolve to Scalar
types:
We know this because those fields donât have any sub-fieldsâthey are the leaves of the query.
GraphQL comes with a set of default Scalar types out of the box:
Int
: A signed 32âbit integer.Float
: A signed double-precision floating-point value.String
: A UTFâ8 character sequence.Boolean
:true
orfalse
.ID
: A unique identifier, often used to refetch an object or as the key for a cache. TheID
type is serialized in the same way as aString
; however, defining it as anID
signifies that it is not intended to be humanâreadable.
In most GraphQL service implementations, there is also a way to specify custom
Scalar types. For example, we could define a Date
type:
scalar Date
Then itâs up to our implementation to define how that type should be serialized,
deserialized, and validated. For example, you could specify that the Date
type
should always be serialized into an integer timestamp, and your client should
know to expect that format for any date fields.
Enum types
Enum types, also known as enumeration types, are a special kind of scalar that is restricted to a particular set of allowed values. This allows you to:
- Validate that any arguments of this type are one of the allowed values
- Communicate through the type system that a field will always be one of a finite set of values
Hereâs what an Enum type definition looks like in SDL:
enum Episode {
NEWHOPE
EMPIRE
JEDI
}
This means that wherever we use the type Episode
in our schema, we expect it
to be exactly one of NEWHOPE
, EMPIRE
, or JEDI
.
GraphQL service implementations in various languages will have a language-specific way of dealing with Enum types. In languages that support enums as a first-class citizen, the implementation might take advantage of that; in a language like JavaScript with no enum support, these values might be internally mapped to a set of integers. However, these details donât leak out to the client, which can operate entirely in terms of the string names of the Enum typeâs values.
Type modifiers
Types are assumed to be nullable and singular by default in GraphQL. However, when you use these named types in a schema (or in query variable declarations) you can apply additional type modifiers that will affect the meaning of those values.
As we saw with the Object type example above, GraphQL supports two type modifiersâthe List and Non-Null typesâand they can be used individually or in combination with each other.
Non-Null
Letâs look at an example:
type Character {
name: String!
}
Here, weâre using a String
type and marking it as a Non-Null type by adding an
exclamation mark (!
) after the type name. This means that our server always
expects to return a non-null value for this field, and if the resolver produces
a null value, then that will trigger a GraphQL execution error, letting the
client know that something has gone wrong.
As we saw in an example above, the Non-Null type modifier can also be used when defining arguments for a field, which will cause the GraphQL server to return a validation error if a null value is passed as that argument:
List
Lists work in a similar way. We can use a type modifier to mark a type as a List
type, which indicates that this field will return an array of that type. In SDL,
this is denoted by wrapping the type in square brackets, [
and ]
. It works
the same for arguments, where the validation step will expect an array for that
value. Hereâs an example:
type Character {
name: String!
appearsIn: [Episode]!
}
As we see above, the Non-Null and List modifiers can be combined. For example,
you can have a List of Non-Null String
types:
myField: [String!]
This means that the list itself can be null, but it canât have any null members. For example, in JSON:
myField: null // valid
myField: [] // valid
myField: ["a", "b"] // valid
myField: ["a", null, "b"] // error
Now, letâs say we defined a Non-Null List of String
types:
myField: [String]!
This means that the list itself cannot be null, but it can contain null values:
myField: null // error
myField: [] // valid
myField: ["a", "b"] // valid
myField: ["a", null, "b"] // valid
Lastly, you can also have a Non-Null List of Non-Null String
types:
myField: [String!]!
This means that the list cannot be null and it cannot contain null values:
myField: null // error
myField: [] // valid
myField: ["a", "b"] // valid
myField: ["a", null, "b"] // error
You can arbitrarily nest any number of Non-Null and List modifiers, according to your needs.
In GraphQL, there is no way to define a type so that the fieldâs data will
only be considered valid if a non-empty list is provided. In other words, []
would still be a valid response for a Non-Null List of a Non-Null type.
Interface types
Like many type systems, GraphQL supports abstract types. The first of these types that weâll explore is an Interface type, which defines a certain set of fields that a concrete Object type or other Interface type must also include to implement it.
For example, you could have a Character
Interface type that represents any
character in the Star Wars trilogy:
interface Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
}
This means that any type that implements Character
needs to have these exact
fields as well as the same arguments and return types.
For example, here are some types that might implement Character
:
type Human implements Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
starships: [Starship]
totalCredits: Int
}
type Droid implements Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
primaryFunction: String
}
You can see that both of these types have all of the fields from the Character
Interface type, but also bring in extra fieldsâtotalCredits
, starships
and
primaryFunction
âthat are specific to that particular type of character.
Interface types are useful when you want to return an object or set of objects, but those might be of several different types. For example, note that the following query produces an error:
The hero
field returns the type Character
, which means it might be either a
Human
or a Droid
depending on the episode
argument. In the query above,
you can only ask for fields that exist on the Character
Interface type, which
doesnât include primaryFunction
.
To ask for a field on a specific Object type, you need to use an inline fragment:
Interface types may also implement other Interface types:
interface Node {
id: ID!
}
interface Character implements Node {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
}
Note that Interface types may not implement themselves or contain any cyclic references to each other.
While types may share common fields in a GraphQL API, that may not always
warrant the use of an Interface type to enforce that those fields remain
consistently named. Interface types are a powerful way to indicate shared
behavior across the types that implement them. When used, they should be
semantically meaningful to client developers, just like Character
is a
useful abstraction for humans and droids.
Union types
GraphQL supports a second abstract type called a Union type. Union types share similarities with Interface types, but they cannot define any shared fields among the constituent types.
A Union type is defined by indicating its member Object types:
union SearchResult = Human | Droid | Starship
Wherever we return a SearchResult
type in our schema, we might get a Human
,
a Droid
, or a Starship
. Note that members of a Union type need to be
concrete Object types; you canât define one using Interface types or other Union
types as members.
In this case, if you query a field that returns the SearchResult
Union type,
you need to use an inline fragment to be
able to query any fields that are defined on the member Object types:
The __typename
field is a special meta-field that automatically exists on
every Object type and resolves to the name of that type, providing a way to
differentiate between data types on the client.
Also, in this case, since Human
and Droid
share a common interface
(Character
), you can query their common fields in one place and still get the
same result:
Note that name
is still specified on Starship
because otherwise it wouldnât
show up in the results given that Starship
is not a Character
!
Input Object types
Most of the examples weâve covered on this page demonstrate how Object, Scalar, Enum, Interface, and Union types may be used as output types for the fields in a schema. But we have also seen that field arguments must specify their input types.
So far, weâve only talked about using scalar values (like Enums or String types) as an input type for a field argument. However, you can also pass complex objects as arguments using an Input Object type, which is the last kind of named types in GraphQL that weâll explore.
This is particularly valuable in the case of
mutations, where you might want to pass in a whole
object to be created. In SDL, Input Object types look similar to regular Object
types, but with the keyword input
instead of type
:
input ReviewInput {
stars: Int!
commentary: String
}
type Mutation {
createReview(episode: Episode, review: ReviewInput!): Review
}
Here is how you could use the Input Object type in a mutation:
The fields on an Input Object type can refer to other Input Object types, but you canât mix input and output types in your schema. Input Object types also canât have arguments on their fields.
Directives
In certain instances where field arguments are insufficient or certain common
behaviors must be replicated in multiple locations,
directives allow
us to modify parts of a GraphQL schema or operation by using an @
character
followed by the directiveâs name.
Type system directives allow us to annotate the types, fields, and arguments in a schema so that they may be validated or executed differently.
Directives may also be defined for use in GraphQL operations as executable directives. Read more about executable directives on the Queries page.
The GraphQL specification defines several
built-in directives.
For example, for implementations that support SDL, the @deprecated
directive
will be available to annotate deprecated parts of the schema:
type User {
fullName: String
name: String @deprecated(reason: "Use `fullName`.")
}
While you wonât need to define the @deprecated
directive in your schema
explicitly if youâre using a GraphQL implementation that supports SDL, itâs
underlying definition would look like this:
directive @deprecated(
reason: String = "No longer supported"
) on FIELD_DEFINITION | ENUM_VALUE
Note that, just like fields, directives can accept arguments and those arguments
can have default values. The @deprecated
directive has a nullable reason
argument that accepts a String
as an input type and falls back to
"No longer supported"
. And with directives, we must specify where they may be
used, such as on a FIELD_DEFINITION
or ENUM_VALUE
for the @deprecated
directive.
In addition to GraphQLâs built-in directives, you may define your own custom directives. As with custom Scalar types, itâs up to your chosen GraphQL implementation to determine how to handle custom directives during query execution.
Documentation
Descriptions
GraphQL allows you to add documentation to the types, fields, and arguments in a schema. In fact, the GraphQL specification encourages you to do this in all cases unless the name of the type, field, or argument is self-descriptive. Schema descriptions are defined using Markdown syntax and can be multiline or single line.
In SDL, they would look like this:
"""
A character from the Star Wars universe
"""
type Character {
"The name of the character."
name: String!
}
"""
The episodes in the Star Wars trilogy
"""
enum Episode {
"Star Wars Episode IV: A New Hope, released in 1977."
NEWHOPE
"Star Wars Episode V: The Empire Strikes Back, released in 1980."
EMPIRE
"Star Wars Episode VI: Return of the Jedi, released in 1983."
JEDI
}
"""
The query type, represents all of the entry points into our object graph
"""
type Query {
"""
Fetches the hero of a specified Star Wars film.
"""
hero(
"The name of the film that the hero appears in."
episode: Episode
): Character
}
In addition to making a GraphQL API schema more expressive, descriptions are helpful to client developers because they are available in introspection queries and visible in developer tools such as GraphiQL.
Comments
Occasionally, you may need to add comments in a schema that do not describe
types, fields, or arguments, and are not meant to be seen by clients. In those
cases, you can add a single-line comment to SDL by preceding the text with a #
character:
# This line is treated like whitespace and ignored by GraphQL
type Character {
name: String!
}
Comments may also be added to client queries:
Next steps
To recap what weâve learned about schemas and types:
- Depending on what library is selected to build a GraphQL service, we can define a schema in a language-agnostic way using SDL or by compiling the schema from the code that provides data for its fields
- There are six kinds of named type definitions in GraphQL: Object, Scalar, Enum, Interface, Union, and Input Object types
- Object types contain fields that specify output types, and those fields may have arguments that specify input types
- The
Int
,ÂFloat
,ÂString
,ÂBoolean
, andÂID
Scalar types are built into GraphQL, and you can also define your own custom scalars - Like Scalar types, Enum types represent leaf values in a GraphQL schema but they are limited to a finite set of values
- The List (
[]
) and Non-Null (!
) type modifiers allow you to change the default behavior for a fieldâs output type or argumentâs input type - Interface and Union types are abstract types that allow different concrete Object types to be output from a single field
- Input Object types allow you to pass more complex values into a field argument or directive argument than Scalar and Enum types allow
- Type system directives can be applied to the types, fields, and arguments in a schema to alter how they are validated and executed during a query
- GraphQL supports schema documentation using type, field, and argument descriptions, and it also supports comments that are ignored by the parser
Now that you understand the key features of the type system, youâre ready to learn more about how to query data from a GraphQL API.