Strawberry — Schema and Features

This section documents the current GraphQL specification (October 2021), plus the working draft for @defer/@stream/@oneOf, and the GraphQL-over-HTTP specification, as published at graphql.org and spec.graphql.org — which are the references these pages are written and verified against — and, for the integration pages, against Spring for GraphQL (2.0.x), Strawberry, Ariadne, and the client docs for Apollo Client / urql / Relay. No specific patch version is pinned. Some surfaces (Apollo Router/managed federation, graphql-ws internals, the Relay compiler internals, GraalVM native) are linked rather than documented in depth.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, as GraphQL tooling iterates quickly.

Beyond the @strawberry.type basics and the FastAPI integration covered on the previous page, Strawberry’s type system and runtime offer a full toolkit for building production schemas: richer field types, access control, batched data loading, structured error reporting, and Relay-style pagination. This page works through each of these in turn, using the code-first, dataclass-driven style Strawberry is built around throughout.

Prerequisites

This page continues directly from Strawberry with FastAPI and assumes the same strawberry-graphql[fastapi] installation. Examples below assume these imports are in scope:

import strawberry
from datetime import datetime
from typing import Generic, TypeVar
from strawberry.dataloader import DataLoader
from strawberry.permission import BasePermission
from strawberry.types import Info

Custom scalars

A custom scalar pairs a GraphQL name with Python serialize/parse_value functions that convert between the wire representation and a native Python type — the same serialize/parseValue/parseLiteral contract covered language-agnostically in Interfaces, Unions \& Inputs:

DateTime = strawberry.scalar(
    datetime,
    name="DateTime",
    serialize=lambda value: value.isoformat(),
    parse_value=lambda value: datetime.fromisoformat(value),
)


@strawberry.type
class Event:
    id: strawberry.ID
    starts_at: DateTime

strawberry.scalar(…​) can wrap any Python type this way, and Strawberry also ships several scalars out of the box (strawberry.ID, JSON, Void) alongside the five SDL-defined scalars (Int, Float, String, Boolean, ID) that map directly onto Python’s int, float, str, and bool.

Enums

A Python enum.Enum becomes a GraphQL enum type by decorating it with @strawberry.enum; each member name is exposed to clients exactly as written, so members are conventionally named in SCREAMING_SNAKE_CASE to match GraphQL enum-value conventions:

import enum


@strawberry.enum
class BookGenre(enum.Enum):
    FICTION = "fiction"
    NON_FICTION = "non_fiction"
    POETRY = "poetry"


@strawberry.type
class Book:
    id: strawberry.ID
    genre: BookGenre

Resolvers can accept and return BookGenre members like any other Python enum value; Strawberry handles the GraphQL-name-to-Python-member translation transparently in both directions.

Interfaces

@strawberry.interface declares a set of fields that multiple object types share; a concrete type opts in by inheriting from the interface class, and Strawberry resolves the correct concrete GraphQL type for each instance automatically from the Python class hierarchy:

@strawberry.interface
class Node:
    id: strawberry.ID


@strawberry.type
class Book(Node):
    title: str


@strawberry.type
class Author(Node):
    name: str

No explicit resolve_type hook is required for this common case — Strawberry inspects the Python class of the value returned from a resolver and matches it against the interface’s registered implementations. A custom resolve_type is only needed when a field returns plain dictionaries or a type Strawberry cannot otherwise map, which is covered alongside `resolveType’s language-agnostic role in Interfaces, Unions \& Inputs.

Unions

A Strawberry union is a typing.Union of @strawberry.type classes, most often built with strawberry.union to give the union type an explicit GraphQL name:

@strawberry.type
class Book:
    title: str


@strawberry.type
class Magazine:
    issue_number: int


SearchResult = strawberry.union("SearchResult", (Book, Magazine))


@strawberry.type
class Query:
    @strawberry.field
    def search(self, term: str) -> list[SearchResult]:
        return repository.search(term)

As with interfaces, Strawberry infers which union member a given value maps to from its Python class, so a resolver simply returns instances of the member types and the schema’s __typename resolution follows without extra configuration.

Generics

A Generic[T] class decorated with @strawberry.type becomes a reusable GraphQL type template — Strawberry generates one concrete GraphQL type per type parameter it is instantiated with, which is the idiomatic way to build a paginated Connection-shaped wrapper without repeating its fields for every entity:

T = TypeVar("T")


@strawberry.type
class Page(Generic[T]):
    items: list[T]
    total_count: int


@strawberry.type
class Query:
    @strawberry.field
    def books(self) -> Page[Book]:
        items, total = repository.list_books()
        return Page(items=items, total_count=total)

Page[Book] above produces a BookPage GraphQL type in the emitted schema; a second call site using Page[Author] would produce a distinct AuthorPage type, each generated from the same generic definition.

Input types

@strawberry.input marks a class as a GraphQL input object type — the argument-structuring construct covered language-agnostically in Interfaces, Unions \& Inputs — and is the idiomatic way to group several mutation arguments into one typed argument:

@strawberry.input
class AddBookInput:
    title: str
    author_id: strawberry.ID
    genre: BookGenre = BookGenre.FICTION


@strawberry.type
class Mutation:
    @strawberry.mutation
    def add_book(self, input: AddBookInput) -> Book:
        return repository.create_book(input.title, input.author_id, input.genre)

Fields with default values, like genre above, become optional on the generated AddBookInput GraphQL type; fields without a default are required arguments a client must supply.

Private fields with strawberry.Private

strawberry.Private[T] marks a dataclass attribute as invisible to GraphQL entirely — it exists on the Python object for resolvers and business logic to read, but Strawberry excludes it from the generated schema and from introspection:

@strawberry.type
class Book:
    id: strawberry.ID
    title: str
    internal_rating_score: strawberry.Private[float]

    @strawberry.field
    def is_highly_rated(self) -> bool:
        return self.internal_rating_score >= 4.5

This is the idiomatic way to carry server-only data alongside a type’s public fields — an internal score used only to compute is_highly_rated above, a raw database row, or a cache key — without accidentally exposing it as a queryable field.

Permission classes

A BasePermission subclass centralizes an authorization check that would otherwise be repeated inside every resolver body; field(permission_classes=[…​]) attaches one or more permission classes to a specific field, and Strawberry runs has_permission before the resolver executes, short-circuiting with the class’s message-carrying error when it returns False:

class IsAuthenticated(BasePermission):
    message = "User is not authenticated"

    def has_permission(self, source: object, info: Info, **kwargs: object) -> bool:
        return info.context["current_user"] is not None


@strawberry.type
class Query:
    secret_message: str = strawberry.field(
        permission_classes=[IsAuthenticated],
        resolver=lambda: "only visible to authenticated users",
    )

Multiple permission classes on the same field are evaluated in order, and any one of them returning False denies access without running the rest. This complements, rather than replaces, the broader authorization model covered in Authorization — a BasePermission is a convenient place for a per-field policy check, but authentication itself and domain-layer authorization decisions still belong where that page describes. See Strawberry — Permissions for the full reference, including class-based composition and async has_permission implementations.

DataLoaders for batching

strawberry.dataloader.DataLoader batches many single-key lookups issued during one GraphQL request into a single call, and caches results for the lifetime of the request so the same key is never fetched twice — Strawberry’s implementation of the batch-and-cache pattern introduced language-agnostically in Performance \& N+1:

async def batch_load_authors(keys: list[strawberry.ID]) -> list[Author]:
    authors_by_id = await repository.get_authors_by_ids(keys)
    return [authors_by_id[key] for key in keys]


@strawberry.type
class Book:
    id: strawberry.ID
    title: str
    author_id: strawberry.ID

    @strawberry.field
    async def author(self, info: Info) -> Author:
        loader: DataLoader[strawberry.ID, Author] = info.context["author_loader"]
        return await loader.load(self.author_id)

The batch function above always receives keys in the exact order .load(…​) was called for them, and must return a same-length list of results in that same order — DataLoader matches results back to callers positionally, not by re-inspecting the keys. A fresh DataLoader instance is expected once per request (usually constructed inside the context_getter covered on the previous page), since caching results across requests would leak one user’s data into another’s. See Strawberry — DataLoaders for batching multiple different key shapes, priming the cache ahead of time, and disabling caching for a specific loader.

Error handling

A resolver that raises a Python exception surfaces as a top-level GraphQL error in the response’s errors array — the envelope shape covered language-agnostically in Response and Error Handling. For expected, client-facing failure cases, the idiomatic Strawberry pattern is instead to model the failure as data using a union return type, so a client can distinguish "found nothing" from "the server broke" via __typename rather than by parsing an error message:

@strawberry.type
class BookNotFoundError:
    message: str
    book_id: strawberry.ID


AddBookResult = strawberry.union("AddBookResult", (Book, BookNotFoundError))


@strawberry.type
class Mutation:
    @strawberry.mutation
    def publish_book(self, book_id: strawberry.ID) -> AddBookResult:
        book = repository.get_book(book_id)
        if book is None:
            return BookNotFoundError(message="No such book", book_id=book_id)
        return repository.publish(book)

Strawberry also exposes strawberry.exceptions.StrawberryGraphQLError for raising an error with custom extensions attached, and a schema-level process_errors hook for logging or reshaping every error before it reaches the response, for the cases where the top-level errors array genuinely is the right place to report a failure.

Relay-style pagination with strawberry.relay

strawberry.relay implements the Relay Cursor Connections specification — Pagination's Connection/Edge/PageInfo shapes — so a paginated field can be declared without hand-writing that boilerplate. relay.Node also implements the Global Object Identification Node interface, giving a type an opaque, globally unique id for free:

from strawberry import relay


@strawberry.type
class Book(relay.Node):
    id: relay.NodeID[int]
    title: str


@strawberry.type
class Query:
    books: relay.ListConnection[Book] = relay.connection(resolver=lambda: repository.list_books())

relay.connection wires up the first/after/last/before arguments and the Connection/Edge/PageInfo response shape automatically from the resolver’s returned iterable; relay.NodeID marks which field on Book becomes the opaque part of its globally unique Relay ID, distinct from any id field the underlying data source already exposes. A schema built this way also gets a node(id:) root field for free, resolving any relay.Node subclass back from its opaque ID exactly as Global Object Identification describes. See Strawberry — Relay for cursor encoding customization, connection resolvers backed by a database LIMIT/OFFSET or keyset query, and the @relay.connection decorator form.

Schema export and codegen

The strawberry export-schema CLI command prints a Strawberry schema’s SDL representation to standard output, which is the usual way to commit a generated .graphql schema file to version control or feed it to another tool’s codegen step:

strawberry export-schema myapp.schema:schema > schema.graphql

myapp.schema:schema above is a Python import path — module myapp.schema, attribute schema — pointing at the strawberry.Schema instance to export. This is useful independent of any particular client: a client-side codegen tool (such as the ones covered in Clients overview) typically needs the SDL on disk rather than a running server to introspect, and keeping the exported file in version control makes schema changes visible in code review.

The mypy plugin

Because Strawberry types are plain dataclasses decorated at runtime, static analyzers cannot infer GraphQL-specific behavior — such as a @strawberry.field-decorated method’s true return type, or that a strawberry.Private attribute should be excluded from constructor checks — without help. Strawberry ships a mypy plugin that teaches mypy these semantics, enabled via pyproject.toml:

[tool.mypy]
plugins = ["strawberry.ext.mypy_plugin"]

With the plugin enabled, mypy type-checks resolver signatures, input type construction, and generic type instantiations (such as the Page[Book] example earlier on this page) with the same accuracy it would apply to an equivalent, non-decorated dataclass.

Where to go from here

This page covered Strawberry’s type-system extensions and runtime features beyond the FastAPI basics; two siblings cover the surrounding ground:

  • Strawberry with FastAPI covers mounting a schema on FastAPI, the context_getter, and subscriptions — the prerequisite this page builds on.

  • Ariadne — schema-first covers the alternative, SDL-first way to build a Python GraphQL server for teams that prefer defining the schema as text.

This page was generated with the assistance of AI. Verify the exact APIs shown here — particularly BasePermission, DataLoader, and the strawberry.relay module — against Strawberry’s Permissions guide, DataLoaders guide, and Relay guide before relying on them in production, since Strawberry’s API surface evolves between releases.