Skip to content

The Type System

We have called the schema “the contract” several times. Now we open it up. The schema is built from a small set of type kinds, written in the Schema Definition Language (SDL). Learn these building blocks and you can read — and design — any GraphQL API.

The most common kind of type is an object type: a named collection of fields. Each field has a name and a type, and may take arguments. Here is an object type describing a track in a music library:

type Track {
id: ID!
title: String!
durationSeconds: Int!
explicit: Boolean
artist: Artist!
}

Track has five fields. Each field’s type tells the client what it will get back: an ID, some String and Int values, a Boolean, and a link to another object type, Artist. That last field is what turns a flat record into a graph — fields can point at other types, and clients can traverse them.

Every selection eventually bottoms out at a scalar: a single, indivisible value. GraphQL ships with five built-in scalars:

  • Int — a signed 32-bit integer.
  • Float — a signed double-precision floating-point value.
  • String — UTF-8 text.
  • Boolean — true or false.
  • ID — a unique identifier, serialized as a string but semantically opaque (you are not meant to do math on it).

You can also define custom scalars (for example DateTime or EmailAddress) to attach validation and serialization rules, but the five above are the foundation.

Three object types are special because they are the entry points into the graph. A query has to start somewhere, and these names are where it starts:

  • Query — the read entry point. Every read begins by selecting a field here.
  • Mutation — the write entry point. Fields here create, update, or delete data.
  • Subscription — the real-time entry point. Fields here stream updates over time.

Only Query is required. A schema with reads but no writes simply omits Mutation and Subscription.

flowchart TD
  Query["Query (read entry)"]
  Mutation["Mutation (write entry)"]
  Subscription["Subscription (stream entry)"]
  Track["type Track"]
  Artist["type Artist"]
  Scalars["Scalars: ID, String, Int, Boolean, Float"]
  Query --> Track
  Mutation --> Track
  Track -->|"artist field"| Artist
  Track --> Scalars
  Artist --> Scalars
Root types are the doorways; object types and scalars form the rest of the graph.

By default every field in GraphQL is nullable: it may return a value or null. Adding a trailing ! makes the field non-null — the server guarantees a real value, never null. Read the ! as “guaranteed”:

type Artist {
id: ID! # always present
name: String! # always present
bio: String # may be null — biography is optional
tracks: [Track!]! # a non-null list of non-null Tracks
}

That last line is worth decoding. [Track!]! is a list type with two exclamation marks doing two different jobs:

  • The inner Track! means every element in the list is a non-null Track.
  • The outer ! means the list itself is never null (it may still be empty, []).

So tracks always returns a list, and that list never contains a null hole. Nullability is part of the contract: it tells clients exactly which fields they must guard against and which they can trust.

The example below defines a two-type schema — Artist and Track — wires up resolvers, and runs a query that crosses from one type into the other. Press Run to execute it with the real GraphQL engine.

JavaScript

Look closely at the output. title and durationSeconds come back as guaranteed values because they are non-null in the schema. artist is a nested object — the query stepped from Track into Artist — and inside it bio comes back as null, which is allowed because bio was declared without a !. The type system predicted every one of these shapes before the resolver ran.

Making every field nullable without thinking

Symptoms:

  • the schema uses String instead of String! everywhere
  • the client has to null-check every field, even ones that can never actually be null
  • Fix: design nullability on purpose — ! when you are confident, nullable when a field may genuinely be absent

A schema that mirrors the database tables verbatim

Symptoms:

  • schema type names match database table names one-to-one
  • every schema field is a database column
  • Fix: don’t expose implementation detail to clients unnecessarily

💡 Real-world examples

GitHub GraphQL API:

  • uses custom scalars such as URI, DateTime, and HTML beyond the built-ins (GitHub GraphQL scalar reference)
  • the type system tells the client exactly which fields return a URL, a date format, or an HTML string

Shopify:

  • uses the Node interface (every object with a global id) so any object can be fetched directly via node/nodes (Shopify Node interface)
  • uses the SearchResultItem union for search results, which can be a Product, Page, or Article (Shopify SearchResultItem)
What does a trailing exclamation mark, as in String!, declare about a field?
Which of these is NOT one of GraphQL’s five built-in scalar types?
Which root type is the read entry point and is the only one a schema must define?
What does the type [Track!]! mean?