Mapping models
@strawchemy.type builds a GraphQL type from a SQLAlchemy model. This page covers choosing its fields, adding your own, and overriding the types Strawchemy generates.
Exposing a model
@strawchemy.type turns a SQLAlchemy model into a GraphQL type. Two lines expose every column and relationship on User:
@strawchemy.type(User, include="all")
class UserType: ...Choosing fields
@strawchemy.type(User, include=["id", "name"])
class UserType: ...Or map every field except one:
@strawchemy.type(User, exclude=["email"])
class UserType: ...Field groups
include and exclude also accept group selectors, importable from strawchemy, instead of listing field names one by one:
SCALARS— every column fieldRELATIONSHIPS— every relationship fieldALL— both; equivalent toinclude=[SCALARS, RELATIONSHIPS]
A bare constant or a list mixing groups and field names both work:
from strawchemy import RELATIONSHIPS, SCALARS
@strawchemy.type(User, include=SCALARS)
class UserType: ...
@strawchemy.type(User, include=[SCALARS, "posts"])
class UserType: ...The first keeps only id, name and email; the second adds the posts relationship on top of the columns. Groups work the same way in exclude — excluding a group leaves everything else included by default:
@strawchemy.type(User, exclude=RELATIONSHIPS)
class UserType: ...include and exclude can be combined: a field is kept when include selects it and exclude doesn't.
@strawchemy.type(User, include=SCALARS, exclude=["email"])
class UserType: ...Custom fields
A ModelInstance[User] attribute gives a resolver access to the underlying model instance. @strawchemy.field doubles as a method decorator, not just a function you call: decorating a method exposes it as a GraphQL field alongside the auto-generated ones. The statement Strawchemy builds loads what the client selected, not what the method reads, so pass a QueryHook naming what the method needs — name and email here:
from strawchemy import ModelInstance, QueryHook
@strawchemy.type(User, include="all")
class UserType:
instance: ModelInstance[User]
@strawchemy.field(query_hook=QueryHook(load=[User.name, User.email]))
def display_name(self) -> str:
return f"{self.instance.name} <{self.instance.email}>"Without the hook, { users { displayName } } fails with sqlalchemy.exc.MissingGreenlet unless the client also selects name and email (a sync session instead runs one extra query per row). A relationship always needs the hook, since relationships the client selects are not set on the instance:
@strawchemy.field(query_hook=QueryHook(load=[User.posts]))
def post_count(self) -> int:
return len(self.instance.posts)load takes columns or relationships, but the rule is the same either way: whatever the method reads, the hook declares.
A decorated method may return any type, a plain @strawberry.type included; a strawchemy.field() declared without one needs a Strawchemy type, as covered in accepted field types.
See query hooks for what QueryHook can do, and custom resolvers for the other options @strawchemy.field accepts.
Overriding generated types
Mapping UserType with include="all" doesn't stop at User: it walks the posts relationship and auto-generates a default PostType. Declaring your own PostType afterward gives you a second type over the same model — but only with override=True:
@strawchemy.type(Post, include="all")
class PostType: ...Without override=True, this raises at class-decoration time:
strawchemy.exceptions.StrawchemyError: Type `PostType` is already registered@strawchemy.type(Post, include="all", override=True)
class PostType: ...override=True tells Strawchemy to use your definition instead of the generated one.
Reusing the same type
scope="schema" is an alternative to override=True, not an addition to it: instead of overriding the auto-generated type after the fact, it registers your type as the canonical one for a model and purpose (a type, filter, input, …) up front, so nothing auto-generates one to override. Declare TagType as schema-scoped before anything maps Post, and its tags field picks it up automatically. Include only SCALARS, not "all" — a relationship field on a schema-scoped type would still walk into its target model and auto-register a type for it, the same collision override=True exists to solve:
@strawchemy.type(Tag, include=SCALARS, scope="schema")
class TagType: ...
@strawchemy.type(Post, include=["id", "title", "tags"])
class PostType: ...Mapping strictness
By default (strict=True), a column with no GraphQL mapping fails only when strawberry.Schema(...) builds the schema; the class itself decorates without complaint. A complex column stored through SQLAlchemy's PickleType is one such column:
from sqlalchemy import PickleType
from sqlalchemy.orm import Mapped, mapped_column
class Widget(Base):
__tablename__ = "widget"
id: Mapped[int] = mapped_column(primary_key=True)
payload: Mapped[complex] = mapped_column(PickleType)
@strawchemy.type(Widget, include="all")
class WidgetType: ...@strawberry.type
class Query:
widgets: list[WidgetType] = strawchemy.field()
schema = strawberry.Schema(query=Query) TypeError: WidgetType fields cannot be resolved. Unexpected type '<class 'complex'>'Set strict=False on the config and the same column is dropped from the type instead, with a warning as its only trace:
strawchemy = Strawchemy(StrawchemyConfig("sqlite", strict=False))Skipping Widget.payload: no GraphQL mapping for <class 'complex'>