GraphQL Schema and Type System
Learn the foundation of GraphQL API design: schemas, object types, scalar types, enums, lists, non-nullability, interfaces, unions, and input object types.
GraphQL Schema Definition Language
GraphQL provides a language called Schema Definition Language (SDL) for describing an API schema.
A simple schema might look like this:
type User {
id: ID!
name: String!
age: Int
}
type Query {
user(id: ID!): User
users: [User!]!
}
SDL is declarative. It describes what the API looks like rather than how data is retrieved.
The User type describes a user object. The Query type describes read operations available to clients.
Notice the distinction between a schema definition and implementation logic. The SDL says that user exists and accepts an ID!. It does not say whether the user comes from MongoDB, PostgreSQL, Redis, another HTTP service, or an in-memory object.
This separation is powerful because the API contract can remain stable while the underlying implementation changes.
A schema is not merely documentation. It is executable metadata used by GraphQL to validate operations and determine how the requested selection should be executed.
type User {
id: ID!
name: String!
email: String!
}
type Query {
user(id: ID!): User
users: [User!]!
}User object and a root Query object. ID! means the ID is non-null. [User!]! means the users field itself cannot be null, and each user element inside the list also cannot be null.- SDL describes the public GraphQL API contract.
- Schema definitions are separate from resolver implementation.
- The schema is used for validation and execution.
- GraphQL types make API capabilities explicit.
What does GraphQL SDL primarily describe?
Scalar Types
Scalar types represent leaf values in a GraphQL response. GraphQL provides five built-in scalar types:
Int— a signed 32-bit integer.Float— a signed double-precision floating-point value.String— a UTF-8 character sequence.Boolean—trueorfalse.ID— an identifier intended to be serialized as a string.
Example:
type Product {
id: ID!
name: String!
quantity: Int!
price: Float!
available: Boolean!
}
A scalar is a leaf because the client cannot select fields beneath it.
For example:
product {
name
price
}
is valid, but this is not:
product {
price {
value
}
}
because Float has no child fields.
ID versus String
An ID represents an identifier. It is commonly used for database identifiers and resource identifiers. Although GraphQL serializes IDs in a string-compatible way, using ID communicates semantic intent: this value identifies something.
Integer versus Float
Use Int for whole numbers and Float for decimal numbers. Be careful with financial data: a GraphQL Float should not automatically be interpreted as an appropriate representation for exact monetary calculations. Production financial systems often use decimal representations at the application or database layer.
32-bit signed integerDouble-precision floating pointUTF-8 texttrue or falseIdentifier- Scalars are GraphQL leaf values.
- GraphQL provides five built-in scalar types.
- ID communicates identifier semantics.
- Scalar types cannot have nested selections.
Which built-in GraphQL scalar is intended specifically to represent identifiers?
Lists and Non-Null Types
GraphQL type modifiers let you express collection and nullability constraints.
The [] syntax represents a list and ! represents Non-Null.
Consider these types:
users: [User]
users: [User]!
users: [User!]
users: [User!]!
They are not equivalent.
[User]
The list may be null, and individual elements may also be null.
[User]!
The list itself cannot be null, but individual elements may be null.
[User!]
The list may be null, but every element that exists must be a User rather than null.
[User!]!
Neither the list nor its elements can be null.
This distinction is important when designing reliable APIs. Nullability is part of the schema contract and affects validation, execution, generated types, and client behavior.
For example:
type Query {
users: [User!]!
}
communicates that a successful resolver result should always be a list and that every element should be a valid non-null User.
Non-null does not mean that the underlying database can never contain missing data. It means the GraphQL field contract promises that a null value cannot be returned at that location during successful execution. If a resolver violates a non-null contract, GraphQL can propagate null upward according to its execution rules and report an error.
Nullable list, nullable elementsNon-null list, nullable elementsNullable list, non-null elementsNon-null list, non-null elements- Square brackets represent lists.
- The exclamation mark represents Non-Null.
- List nullability and element nullability are independent.
- Nullability is an important part of API design.
What does `[User!]!` mean?
Enums, Interfaces and Unions
GraphQL provides several type-system features for modeling domains that are more complex than simple objects.
Enums
An enum restricts a value to a predefined set of named values.
enum OrderStatus {
PENDING
CONFIRMED
SHIPPED
DELIVERED
CANCELLED
}
This is preferable to accepting arbitrary strings when the domain has a known finite set of states.
Interfaces
An interface defines a set of fields that implementing object types must provide.
interface Node {
id: ID!
}
type User implements Node {
id: ID!
name: String!
}
type Product implements Node {
id: ID!
name: String!
}
Interfaces are useful when multiple object types share a common contract.
Unions
A union represents a value that may be one of several object types without requiring those types to share fields.
union SearchResult = User | Product | Article
A client querying a union usually needs fragments to select fields specific to the concrete type.
search(term: "graphql") {
... on User {
id
name
}
... on Product {
id
name
price
}
}
The distinction is important:
- Interface: different types share a defined field contract.
- Union: different types are grouped as possible result types, but the union itself does not require shared fields.
Finite set of named valuesShared field contractOne of several object types- Enums constrain values to predefined names.
- Interfaces define shared fields across implementing object types.
- Unions represent one of several possible object types.
- Inline fragments are commonly used when querying unions and interfaces.
What is the main difference between an interface and a union?
Input Object Types
GraphQL distinguishes between output object types and input object types.
An object type describes data that can be returned to a client:
type User {
id: ID!
name: String!
}
An input object type describes structured values supplied by the client:
input CreateUserInput {
name: String!
email: String!
}
You can then use it in a mutation:
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
Input types are especially useful when an operation accepts multiple related parameters. Instead of creating a mutation with many individual arguments:
createUser(name: String!, email: String!, age: Int, city: String)
a structured input is easier to evolve:
createUser(input: CreateUserInput!): User!
Input object types can contain scalars, enums, lists, nested input objects, and non-null constraints. They cannot directly be used as output object types.
A good schema design usually treats input types as explicit contracts for client-supplied data rather than passing arbitrary JSON objects through the API.
input CreateProductInput {
name: String!
description: String
price: Float!
category: ProductCategory!
}
enum ProductCategory {
ELECTRONICS
BOOKS
CLOTHING
}
type Mutation {
createProduct(input: CreateProductInput!): Product!
}CreateProductInput type groups related client-provided values. The mutation accepts exactly one required input object. This structure makes the operation easier to understand and provides a clean place to add future input fields.- Input object types model structured client input.
- Output object types and input object types serve different purposes.
- Input objects make mutations easier to evolve.
- Input types can contain nested input structures and GraphQL scalar types.
Why are GraphQL input object types commonly used in mutations?