GraphQL schemas for a service are now most often specified using what’s known as the GraphQL SDL (schema definition language), also sometimes referred to as just GraphQL schema language. It’s a language with a very simple syntax that allows to define a schema very succinctly. Once you have a gasp at the different syntax elements of the SDL, you’ll be able to write schemas in no time flat.

The Basics
Here’s what a basic GraphQL schema deffinition for a simple todo app could look like:
# Enumeration type for a level of priority
enum Priority {
LOW
MEDIUM
HIGH
}
# Our main todo type
type Todo {
id: ID!
name: String!
description: String
priority: Priority!
}
type Query {
# Get one todo item
todo(id: ID!): Todo
# Get all todo items
allTodos: [Todo!]!
}
type Mutation {
addTodo(name: String!, priority: Priority = LOW): Todo!
removeTodo(id: ID!): Todo!
}
schema {
query: Query
mutation: Mutation
}
A lot is going on here, so let’s break down some of the most important things:
Object Types
Object types are specific to a GraphQL service, are defined with the type keyword and start with a capital letter by convention. They define the type name and the fields present under that type. Each field in an object type can be resolve to either other object types or scalar types. Scalar types point to actual data and represent the leaves of the graph.
In the above schema deffinition we have the Todo object type as well as the Query and Mutation root object types. Only the Query root type is required in all GraphQL schemas, but the mutation root type will most often also be present when the service allows for updating, adding or deleting data. Additionally, a Subscription root type is also available, to define operations that a client can subscribe to.
Built-In Scalar Types
There are 5 built-in scalar types with GraphQL: Int, Float, String, Boolean and ID. Scalar types, as opposed to object types, point to actual data. The ID type resolves to a string, but expects a unique value.
Enumeration Types
Enumeration types allow to define a specific subset of possible values for a type. In the previous example, the Priority enum type can take a value of LOW, MEDIUM or HIGH and anything else will result in a validation error. On the client strings are used to provide a value for an enum type.
Type Modifiers
As you can also see from the above example, modifiers can be used on the type that a field resolves to by using characters like ! and […]. Here’s a breakdown, using the String scalar type as an example:
- String: nullable string (the resolved value can be null)
- String!: Non-nullable string (if the resolved value is null, an error will be raised)
- [String]: Nullable list of nullable string values. The entire value can be null, or specific list elements can be null.
- [String!]: Nullable list of non-nullable string values. Then entire value can be null, but specific list elements cannot be null.
- [String!]!: Non-nullable list of non-nullable string values. Nothing can be null, neither the whole value nor the individual items. An empty list ([]) is still valid because the whole value is not null and there’s no individual null values.
Comments
Comments are added with the # symbol and only single-line comments are allowed.
Custom Scalar Types
It’s also possible to define custom scalar types with a syntax like this:
scalar DateTime
With this though, the GraphQL service will need to define how the custom scalar is to be serialized and validated.
Union Types
Union types define a type that can resolve to a number of possible object types:
# ...
union Vehicule = Car | Boat | Plane
type Query {
getVehicule(id: ID!): Vehicule!
}
With union types, on the client, inline fragments have to be used to select the desired fields depending on what subtype is being resolved:
query {
getVehicule {
...on Car {
year
}
...on Boat {
color
}
...on Plane {
seating
}
}
}
Interfaces
Interfaces are somewhat similar to union types, but they allow multiple object types to share some fields:
interface Vehicule {
color: String
make: String
speed: Int
}
type Car implements Vehicule {
color: String
make: String
speed: Int
model: String
}
# ...
Each type that implements an interface need to have fields corresponding to all the interface’s fields, but can also have aditional fields of their own. This way, on the client, inline fragments can be used to get fields that are unique to certain types:
graphql {
getVehicule {
color
make
...on Car {
model
}
}
}
Input Types
When a query or mutation expects multiple arguments, it can be easier to define input types where each field represents an argument:
# ...
input NewTodoInput {
name: String!
priority: Priority = LOW
}
type Mutation {
addTodo(newTodoInput: NewTodoInput!): Todo!
removeTodo(id: ID!): Todo!
}
Schema Documentation
There’s also a syntax to add human-readable documentation for types and fields, which can become really helpful when using a tool like GraphiQL or GraphQL Playground to browse the documentation for a schema.
Let’s take our initial todo schema example and add some documentation for the types and some of the fields:
"""
Priority level
"""
enum Priority {
LOW
MEDIUM
HIGH
}
type Todo {
id: ID!
name: String!
"""
Useful description for todo item
"""
description: String
priority: Priority!
}
"""
Queries available on the todo app service
"""
type Query {
"""
Get one todo item
"""
todo(id: ID!): Todo
"""
List of all todo items
"""
allTodos: [Todo!]!
}
type Mutation {
addTodo(
"Name for the todo item"
name: String!
"Priority levl of todo item"
priority: Priority = LOW): Todo!
removeTodo(id: ID!): Todo!
}
schema {
query: Query
mutation: Mutation
}
As you can see, documentation for types or fields is added by wrapping with the ””” … “”” syntax. The ” … “ is used for documentaion on arguments.
🚀 With this you should be off to the races with defining GraphQL schemas. You can also always refer to the official spec when you're unsure about some of the syntax.