Ctrl + K
GraphQL17 min read

GraphQL Schema Basics

A practical introduction to GraphQL schemas covering SDL syntax, types, fields, arguments, scalars, enums, interfaces, unions, input objects, and root operations.

Published: 2026-10-05

A GraphQL schema defines the structure of data and operations available through a GraphQL API. It describes which types exist, which fields those types contain, what arguments fields accept, which values are required, and which operations clients can execute.

Unlike an API where the available data structure may need to be inferred from multiple endpoints or external documentation, a GraphQL schema acts as a strongly typed contract between the client and server. Clients can use the schema to understand what they can request and which values they can provide.

This guide explains the fundamentals of GraphQL schemas, including Schema Definition Language (SDL), object types, scalar types, fields, arguments, nullability, lists, enums, interfaces, unions, input objects, and the Query, Mutation, and Subscription root types.

What Is a GraphQL Schema?

A GraphQL schema is a typed definition of the capabilities of a GraphQL API. It describes the types and fields that clients can access and the operations they can perform.

type User {
  id: ID!
  name: String!
  email: String
}

type Query {
  user(id: ID!): User
  users: [User!]!
}

This schema defines a User object with id, name, and email fields. It also defines a Query type that allows clients to request one user by ID or retrieve a list of users.

The schema does not necessarily contain the implementation that retrieves the data. It defines the API contract, while server-side resolver functions or other execution logic determine how the requested data is actually obtained.

GraphQL Schema Definition Language

GraphQL schemas are commonly written using Schema Definition Language, usually abbreviated as SDL. SDL provides a declarative syntax for describing GraphQL types and their relationships.

type Product {
  id: ID!
  name: String!
  price: Float!
}

The type keyword declares an object type. Each field has a name and an output type. The exclamation mark indicates that the field is non-null.

Basic GraphQL Type Syntax

A basic object type consists of a type name followed by a set of fields. Each field specifies the type of value it can return.

type User {
  id: ID!
  name: String!
  age: Int
  active: Boolean!
}
SyntaxMeaning
StringA nullable string field
String!A non-null string field
[String]A nullable list of nullable strings
[String!]A nullable list of non-null strings
[String]!A non-null list of nullable strings
[String!]!A non-null list of non-null strings

GraphQL Object Types

Object types are the main building blocks of most GraphQL schemas. They describe entities that clients can query and the fields available on those entities.

type Product {
  id: ID!
  name: String!
  description: String
  price: Float!
  inStock: Boolean!
}

A Product object has five fields. The client can request any fields exposed by the schema, subject to authorization and other server-side rules.

query {
  product(id: "42") {
    id
    name
    price
    inStock
  }
}

The schema determines that these fields exist and specifies their return types. This allows GraphQL tooling to validate the operation before execution.

GraphQL Fields

A field represents a value that can be requested from an object type. Fields have names and output types and can optionally accept arguments.

type User {
  id: ID!
  name: String!
  email: String
  posts: [Post!]!
}

The posts field returns a list of Post objects. The schema therefore describes not only individual scalar values but also relationships between different object types.

GraphQL Scalar Types

Scalar types represent leaf values that do not contain nested fields. GraphQL provides five built-in scalar types: Int, Float, String, Boolean, and ID.

ScalarPurposeExample
IntSigned 32-bit integer42
FloatSigned double-precision floating-point value19.95
StringUTF-8 text"Anna"
BooleanTrue or falsetrue
IDUnique identifier"user-42"
type Product {
  id: ID!
  name: String!
  quantity: Int!
  price: Float!
  active: Boolean!
}

A schema can also define custom scalar types when the built-in scalars do not adequately describe a value.

Custom Scalar Types

Custom scalars allow a GraphQL server to represent application-specific values such as dates, timestamps, UUIDs, URLs, decimal values, or other formats.

scalar DateTime

type Event {
  id: ID!
  title: String!
  createdAt: DateTime!
}

The GraphQL specification does not define the serialization format of an application-specific scalar such as DateTime. The server implementation determines how values are parsed and serialized.

Fields with Arguments

Fields can accept arguments that influence the value returned by the server. Arguments are defined in the schema alongside the field.

type Query {
  user(id: ID!): User
  products(limit: Int, offset: Int): [Product!]!
}

The user field requires an id argument, while products accepts optional limit and offset arguments.

query {
  user(id: "42") {
    id
    name
  }

  products(limit: 20, offset: 0) {
    id
    name
    price
  }
}

GraphQL Nullability

GraphQL uses the exclamation mark to indicate that a type is non-null. Without !, a field or input value can generally be null.

type User {
  id: ID!
  name: String!
  nickname: String
}

Here, id and name must return non-null values, while nickname may be null.

⚠️ Non-null is part of the API contract. Marking a field with ! means clients can rely on that field being present when the response is successfully produced according to GraphQL's execution rules.

GraphQL List Types

Square brackets define list types in GraphQL. Lists can contain nullable or non-null elements, and the list itself can also be nullable or non-null.

type User {
  tags: [String]
  roles: [String!]!
}

The tags field is a nullable list whose elements can be null. The roles field must return a non-null list, and every element in that list must also be non-null.

The Query Root Type

The Query root type defines the fields clients can use to read data. A schema normally exposes it through a type named Query.

type Query {
  user(id: ID!): User
  users: [User!]!
  product(id: ID!): Product
}

A client can select any available root query field and then traverse the fields exposed by the returned object types.

query {
  user(id: "42") {
    id
    name
    posts {
      id
      title
    }
  }
}

The Mutation Root Type

The Mutation root type defines state-changing operations. It commonly contains fields for creating, updating, deleting, or otherwise modifying application data.

type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User
  deleteUser(id: ID!): Boolean!
}

The mutation fields and their behavior are determined by the API schema. GraphQL does not require every API to use names such as createUser or updateUser.

mutation {
  createUser(
    input: {
      name: "Anna"
      email: "[email protected]"
    }
  ) {
    id
    name
    email
  }
}

The Subscription Root Type

A Subscription root type can define operations for receiving updates over time. Subscriptions are commonly used for real-time functionality such as notifications, live dashboards, or chat events.

type Subscription {
  messageCreated: Message!
}

The exact transport mechanism for GraphQL subscriptions depends on the server and client ecosystem. The schema describes the subscription field and its result type, while the surrounding implementation handles event delivery.

GraphQL Enums

Enum types define a fixed set of allowed named values. They are useful when a field should accept one value from a predefined set.

enum ProductStatus {
  ACTIVE
  ARCHIVED
  DRAFT
}

type Product {
  id: ID!
  status: ProductStatus!
}

A client can then use one of the values defined by ProductStatus.

query {
  products(status: ACTIVE) {
    id
    name
    status
  }
}

GraphQL Input Object Types

Input object types are used for structured values supplied to fields. They are particularly common with mutations.

input CreateUserInput {
  name: String!
  email: String!
  age: Int
}

type Mutation {
  createUser(input: CreateUserInput!): User!
}

The input object defines exactly which fields can be supplied to createUser. Input objects are separate from regular object types because they are designed for input rather than output.

mutation {
  createUser(
    input: {
      name: "Anna"
      email: "[email protected]"
      age: 28
    }
  ) {
    id
    name
    email
  }
}

Object Types vs Input Types

GraphQL distinguishes between types used to return data and types used to receive input. An object type can contain fields that return other objects, while an input object is designed to represent client-provided values.

TypePurposeExample
typeOutput dataUser
inputInput dataCreateUserInput
enumFixed set of valuesUserRole
scalarLeaf valueDateTime
interfaceShared output fieldsNode
unionOne of several output typesSearchResult

GraphQL Interfaces

An interface defines a set of fields that multiple object types can implement. It is useful when several types share a common structure.

interface Node {
  id: ID!
}

type User implements Node {
  id: ID!
  name: String!
}

type Product implements Node {
  id: ID!
  name: String!
}

Both User and Product implement the Node interface, so they are required to provide the id field defined by that interface.

Interfaces are especially useful when a query can return multiple concrete object types that share a common set of fields.

GraphQL Unions

A union represents a value that can be one of several object types. Unlike an interface, a union does not require its member types to share a common set of fields.

union SearchResult = User | Product

type User {
  id: ID!
  name: String!
}

type Product {
  id: ID!
  name: String!
}

A query returning SearchResult can contain either a User or Product. Clients can use inline fragments to request fields specific to each concrete type.

query Search($term: String!) {
  search(term: $term) {
    __typename

    ... on User {
      id
      name
    }

    ... on Product {
      id
      name
    }
  }
}

Relationships Between GraphQL Types

GraphQL schemas can represent relationships between types by using object fields. A User can have posts, a Product can have a category, and an Order can contain multiple items.

type User {
  id: ID!
  name: String!
  posts: [Post!]!
}

type Post {
  id: ID!
  title: String!
  author: User!
}

These relationships allow clients to request related data in a single GraphQL operation.

query {
  user(id: "42") {
    id
    name
    posts {
      id
      title
    }
  }
}

Schema Type References

GraphQL type references can become nested combinations of named types, lists, and non-null modifiers. Understanding these combinations is important when reading a schema.

type Query {
  users: [User!]!
  user(id: ID!): User
}
Type expressionInterpretation
UserNullable User object
User!Non-null User object
[User]Nullable list of nullable Users
[User!]Nullable list of non-null Users
[User]!Non-null list of nullable Users
[User!]!Non-null list of non-null Users

Schema Directives

Directives provide additional metadata or instructions associated with parts of a GraphQL schema or operation. GraphQL includes built-in directives such as @deprecated and @specifiedBy, while servers can define custom directives.

type User {
  id: ID!
  username: String!
  oldName: String @deprecated(reason: "Use username instead")
}

The @deprecated directive tells clients that a field should no longer be preferred. GraphQL development tools can use this information to warn developers or visually distinguish deprecated fields.

Descriptions in GraphQL Schemas

GraphQL SDL supports descriptions for types and fields. Descriptions are written as string literals immediately before the schema element they describe.

"""
A registered application user.
"""
type User {
  "Unique identifier."
  id: ID!

  "Display name shown to other users."
  name: String!
}

Schema descriptions can be exposed through introspection and displayed by GraphQL documentation and schema exploration tools.

The Schema Definition

A GraphQL schema can explicitly specify its root operation types with the schema definition.

schema {
  query: Query
  mutation: Mutation
  subscription: Subscription
}

type Query {
  user(id: ID!): User
}

type Mutation {
  updateUser(id: ID!, name: String!): User
}

type Subscription {
  userUpdated: User!
}

When the conventional root type names Query, Mutation, and Subscription are used, the explicit schema definition is often unnecessary. GraphQL allows the schema to use those types as the corresponding root operation types by convention.

GraphQL Schema Introspection

GraphQL supports introspection, which allows clients and development tools to request information about the schema itself. Introspection can reveal available types, fields, arguments, descriptions, and other schema metadata when the server permits it.

query {
  __schema {
    types {
      name
    }
  }
}

The special __schema field is part of GraphQL's introspection system. Another commonly used introspection field is __type, which can retrieve information about a specific named type.

query {
  __type(name: "User") {
    name
    kind
    fields {
      name
    }
  }
}

GraphQL IDEs and schema viewers commonly use introspection to build interactive documentation and autocomplete features.

Schema Validation

A GraphQL schema itself must satisfy GraphQL's type-system rules. Problems such as invalid type references, duplicate definitions, incompatible implementations, or incorrect root operation configuration can make a schema invalid.

Once a valid schema is available, GraphQL can validate client operations against it. This means many mistakes can be detected before resolver code executes.

query {
  user(id: "42") {
    id
    unknownField
  }
}

If unknownField is not defined on User, the operation is invalid according to the schema. A GraphQL server or development tool can report this before attempting to execute the request.

Schema and Resolvers

The schema describes what the API exposes, while resolvers or equivalent server-side execution logic provide the actual values. The schema and implementation therefore have different responsibilities.

type Query {
  user(id: ID!): User
}

type User {
  id: ID!
  name: String!
}

For this schema, the server needs execution logic capable of resolving the user field and, when necessary, the fields of the returned User object.

The schema itself does not specify whether the data comes from a SQL database, another API, a file, an in-memory store, or some other source.

Schema-First and Code-First Approaches

GraphQL APIs can be developed using different approaches. In a schema-first workflow, developers define the GraphQL schema explicitly and implement server logic to satisfy it. In a code-first workflow, application code and type definitions are used to generate or construct the GraphQL schema.

ApproachGeneral idea
Schema-firstDefine the GraphQL schema first, then implement the server behavior
Code-firstDefine types and behavior in application code and generate or construct the schema

Neither approach changes the fundamental GraphQL type system. Clients ultimately interact with the resulting schema and its exposed operations.

A Small Complete GraphQL Schema

The following example combines several of the concepts covered in this guide: object types, an enum, an input object, query fields, mutation fields, arguments, lists, and non-null types.

enum ProductStatus {
  ACTIVE
  ARCHIVED
  DRAFT
}

type Product {
  id: ID!
  name: String!
  price: Float!
  status: ProductStatus!
}

input CreateProductInput {
  name: String!
  price: Float!
}

type Query {
  product(id: ID!): Product
  products: [Product!]!
}

type Mutation {
  createProduct(input: CreateProductInput!): Product!
}

A client could query the products field like this:

query {
  products {
    id
    name
    price
    status
  }
}

It could also create a product using the mutation defined by the schema.

mutation {
  createProduct(
    input: {
      name: "Mechanical Keyboard"
      price: 120
    }
  ) {
    id
    name
    price
    status
  }
}

Common GraphQL Schema Mistakes

  • Using an output object type where an input object type is required.
  • Forgetting that fields returning object types require a selection set in client operations.
  • Using the wrong scalar type for an argument or field.
  • Adding ! without considering whether the value can genuinely always be non-null.
  • Confusing [Type] with [Type!]! and other list/nullability combinations.
  • Defining an argument with a type that is incompatible with how the field is used.
  • Forgetting to expose a field through the appropriate root operation type.
  • Assuming GraphQL schema names are standardized across all APIs.
  • Using a field that has been deprecated instead of its replacement.
  • Defining an input object with fields that cannot represent the required client input.
  • Assuming the schema determines where data is stored.
  • Ignoring schema descriptions and deprecation information when integrating an API.

How to Read a GraphQL Schema

When working with an unfamiliar GraphQL API, start with the Query, Mutation, and Subscription root types. These show the main operations exposed to clients.

  • Find the Query type to identify available read operations.
  • Find the Mutation type to identify state-changing operations.
  • Check the Subscription type if the API supports real-time operations.
  • Follow each field's return type to understand the available data.
  • Inspect field arguments and their types.
  • Check which fields and arguments are non-null.
  • Look for input objects used by mutations.
  • Inspect enums to find allowed named values.
  • Follow interfaces and unions to understand polymorphic results.
  • Read field descriptions and deprecation notices.

Schema-aware tooling can make this process much easier because it can present the type relationships visually and provide autocomplete while writing operations.

GraphQL Schema Best Practices

  • Use clear and descriptive names for types and fields.
  • Choose nullability deliberately instead of adding ! everywhere.
  • Use input objects for complex mutation input.
  • Use enums when a value should come from a predefined set.
  • Use interfaces or unions when the API genuinely needs polymorphic results.
  • Provide useful descriptions for public types and fields.
  • Deprecate fields instead of removing them abruptly when clients still depend on them.
  • Keep schema changes backward compatible when possible.
  • Avoid exposing unnecessary implementation details through field names.
  • Use schema validation and automated checks during development.
  • Keep the schema and server implementation synchronized.
  • Review introspection output when testing the public API contract.

Frequently Asked Questions

What is a GraphQL schema?

A GraphQL schema is a typed contract that defines the types, fields, arguments, and operations available through a GraphQL API. It tells clients what they can request and what input they can provide.

What is GraphQL SDL?

GraphQL Schema Definition Language, or SDL, is a declarative syntax used to describe GraphQL schemas. It can define object types, fields, arguments, enums, interfaces, unions, input objects, scalars, directives, and root operation types.

What are Query and Mutation in a GraphQL schema?

Query defines fields used for reading data, while Mutation defines fields intended for state-changing operations. They are the root operation types of a GraphQL API.

What does ! mean in a GraphQL schema?

The exclamation mark indicates a non-null type. For an output field, it means the field is not expected to return null under normal successful execution. For input types, it indicates that a value is required and cannot be null.

What is the difference between type and input in GraphQL?

A type normally describes output objects that clients can query, while an input object describes structured values that clients can provide to fields, especially mutations.

What is GraphQL introspection?

Introspection is GraphQL's mechanism for querying information about the schema itself. It can expose types, fields, arguments, descriptions, and other schema metadata when enabled by the server.

How can I view a GraphQL schema?

If the server permits introspection, a GraphQL schema viewer or development IDE can retrieve the schema and display its types, fields, arguments, and relationships. Some APIs also provide generated schema documentation separately.

Helpful GraphQL Tools

Schema viewers are useful for exploring GraphQL types, fields, arguments, and relationships without reading a large SDL document manually. Query formatters make GraphQL operations easier to read, endpoint testers can send operations directly to an API, and response formatters can help inspect returned data and errors. JSON tree viewers are also useful when examining structured GraphQL responses or introspection results.

Conclusion

A GraphQL schema is the foundation of a GraphQL API. It defines the available types, fields, arguments, input structures, relationships, and root operations that clients can use.

The most important concepts to understand are object types, scalar types, fields, arguments, nullability, lists, enums, input objects, interfaces, unions, and the Query and Mutation root types. Once these pieces are familiar, reading GraphQL documentation and writing queries or mutations becomes much easier.

Because GraphQL operations are validated against the schema, the schema serves as more than documentation: it is a typed contract that connects clients and servers and enables powerful tooling such as autocomplete, validation, introspection, and interactive API exploration.

Found an issue?

Found an error, outdated information, or something missing from this article? Let me know through the Contact page.

Your feedback helps improve our articles and keep them accurate and useful.