Unified, backend-agnostic ORM abstraction for Strawberry GraphQL
Project description
strawberry-orm
Backend-agnostic schema generation for Strawberry GraphQL on top of Django ORM, SQLAlchemy, and Tortoise ORM.
WARNING
strawberry-ormis still in alpha. Expect breaking changes, incomplete APIs, and release-to-release churn while the package stabilizes.
strawberry-orm helps you keep one Strawberry schema style across multiple ORMs. It focuses on:
- model-backed Strawberry types
- generated input, filter, and order types
- list fields that expose filtering and ordering automatically
- query optimization hooks to reduce N+1 queries
- helpers for related-list mutation inputs
Installation
# Base package
uv add strawberry-orm
# With a backend
uv add strawberry-orm[django]
uv add strawberry-orm[sqlalchemy]
uv add strawberry-orm[tortoise]
You can do the same with pip:
pip install "strawberry-orm[sqlalchemy]"
Requirements:
- Python
>=3.10 strawberry-graphql>=0.311.0
Quick Start
- Create a backend instance.
- Generate Strawberry types from ORM models with
@orm.type(...). - Generate filter/order types from the same models.
- Expose list fields with
orm.field(). - Add
orm.optimizer_extension()to the schema.
import strawberry
from strawberry_orm import StrawberryORM, auto
orm = StrawberryORM(
"sqlalchemy",
dialect="postgresql",
session_getter=lambda info: info.context["session"],
)
UserFilter = orm.filter(User)
UserOrder = orm.order(User)
@orm.type(User, filters=UserFilter, order=UserOrder)
class UserType:
id: auto
name: auto
email: auto
@strawberry.type
class Query:
users: list[UserType] = orm.field()
schema = strawberry.Schema(
query=Query,
extensions=[orm.optimizer_extension()],
)
That single users field will:
- start from the backend's default queryset for
User - accept
filterandorderarguments automatically becauseUserTypecarries them - let the optimizer eagerly load related data based on the GraphQL selection set
Backends
| Backend | Constructor | Notes |
|---|---|---|
| Django | StrawberryORM("django") |
Uses Django querysets directly. |
| SQLAlchemy | StrawberryORM("sqlalchemy", dialect="postgresql", session_getter=...) |
Requires a SQLAlchemy session at resolve time. |
| Tortoise | StrawberryORM("tortoise") |
Async ORM; use Strawberry in async mode. |
Django
orm = StrawberryORM("django")
SQLAlchemy
orm = StrawberryORM(
"sqlalchemy",
dialect="postgresql",
session_getter=lambda info: info.context["session"],
)
SQLAlchemy needs a session when a query is executed. strawberry-orm can obtain it from either:
session_getter=...info.context["session"]info.context.sessioninfo.context.get_session()
If your context stores a callable session factory, pass a session_getter instead of putting the callable directly on info.context.
Tortoise
orm = StrawberryORM("tortoise")
Backend Options
Shared options:
| Option | Default | Meaning |
|---|---|---|
default_query_limit |
None |
Adds a default limit to list queries created from the backend default queryset. |
exclude_sensitive_fields |
True |
Excludes sensitive-looking fields from generated input/filter/order types. |
warn_sensitive |
True |
Emits warnings when sensitive-looking fields are exposed on generated output types. |
hard_delete_refs |
False |
Makes apply_ref_list(..., delete=...) delete related rows instead of only unlinking them. |
max_filter_depth |
10 |
Caps recursive filter nesting. |
max_filter_branches |
50 |
Caps the total number of all / any / oneOf branches. |
max_in_list_size |
500 |
Caps inList / notInList filter size. |
enable_regex_filters |
False |
Enables regex and iRegex string lookups. |
SQLAlchemy-only options:
| Option | Default | Meaning |
|---|---|---|
dialect |
"postgresql" |
Chooses SQLAlchemy dialect-specific behavior. |
session_getter |
None |
Returns the session for the current request. |
filter_overrides |
{} |
Maps Python types to custom lookup input types. |
Defining Types
@orm.type(Model)
Use @orm.type(Model) to turn an ORM model into a Strawberry object type.
from strawberry_orm import auto
@orm.type(User)
class UserType:
id: auto
name: auto
email: auto
auto is an alias for strawberry.auto. The backend inspects the model and fills in the Python type for each field.
Keyword arguments:
include=[...]exclude=[...]name="CustomGraphQLTypeName"filters=UserFilterorder=UserOrder
@orm.type(User, include=["id", "name"], name="PublicUser")
class PublicUserType:
id: auto
name: auto
@orm.type(User, exclude=["password_hash", "api_key"])
class SafeUserType:
id: auto
name: auto
email: auto
Relations
Reference other generated types directly. The backend auto-generates resolvers for relationship fields:
@orm.type(Tag)
class TagType:
id: auto
name: auto
@orm.type(Post)
class PostType:
id: auto
title: auto
tags: list[TagType]
If the nested type carries filters and/or order, list relations expose those arguments too.
Custom Strawberry Fields
You can mix generated fields with plain Strawberry fields:
@orm.type(User)
class UserType:
id: auto
name: auto
email: auto
@strawberry.field
def display_name(self) -> str:
return f"{self.name} <{self.email}>"
Type-Level Queryset Scoping
Define a get_queryset classmethod on a type to scope the model query centrally. When the optimizer extension is installed and a resolver returns a backend query object, get_queryset is applied automatically.
@orm.type(Post)
class PublishedPostType:
id: auto
title: auto
is_published: auto
@classmethod
def get_queryset(cls, qs, info):
return qs.filter(is_published=True) # Django / Tortoise style
# return qs.where(Post.is_published == True) # SQLAlchemy style
This is useful for soft-delete filtering, multi-tenant scoping, "published only" content types, and reusable authorization-aware model filters.
orm.input(Model)
Generates a Strawberry input type from model metadata.
CreateUserInput = orm.input(User, include=["name", "email"])
Generated input fields are optional (defaulting to strawberry.UNSET), skip relations, exclude primary keys by default, and exclude sensitive-looking fields unless explicitly included.
Keyword arguments: include, exclude, exclude_pk=False, name.
orm.partial(Model)
UpdateUserInput = orm.partial(User, include=["name", "email"])
Same logic as input() with a default name like UserPartialInput. Useful for patch-style update payloads.
Filters and Ordering
Filters
Generate a filter input and attach it to a type:
UserFilter = orm.filter(User)
@orm.type(User, filters=UserFilter)
class UserType:
id: auto
name: auto
email: auto
List fields returning UserType then accept a filter argument:
{
users(filter: { field: { name: { exact: "Alice" } } }) {
id
name
}
}
Filter Shape
Filters are recursive @oneOf trees supporting field, all, any, not, and oneOf:
# OR condition
{
users(filter: {
any: [
{ field: { name: { exact: "Alice" } } }
{ field: { name: { exact: "Bob" } } }
]
}) { name }
}
# AND condition
{
posts(filter: {
all: [
{ field: { authorId: { exact: 1 } } }
{ field: { isPublished: { exact: true } } }
]
}) { title }
}
# NOT condition
{
users(filter: {
not: { field: { email: { contains: "example.com" } } }
}) { name }
}
Built-in Lookup Types
The package exports reusable lookup inputs:
StringLookup, BooleanLookup, IDLookup, IntComparisonLookup, FloatComparisonLookup, DateComparisonLookup, TimeComparisonLookup, DateTimeComparisonLookup
Typical string lookups: exact, neq, contains, iContains, startsWith, iStartsWith, endsWith, iEndsWith, inList, notInList, isNull.
Regex lookups (regex, iRegex) are disabled by default. Enable with enable_regex_filters=True.
Ordering
Generate an order input:
UserOrder = orm.order(User)
The generated type is a @oneOf input — each entry specifies exactly one column.
The order argument is a list, where position determines tie-break priority:
{
users(order: [{ name: ASC }, { email: DESC }]) {
name
email
}
}
This sorts by name ascending first, then breaks ties by email descending.
Supported values from the Ordering enum: ASC, ASC_NULLS_FIRST, ASC_NULLS_LAST, DESC, DESC_NULLS_FIRST, DESC_NULLS_LAST.
Filters and ordering can be combined:
{
posts(
filter: { field: { isPublished: { exact: true } } }
order: [{ title: DESC }]
) {
title
}
}
Queries
Automatic List Fields
If a field returns list[SomeType], orm.field() builds the resolver from the model attached to that type:
@orm.type(User, filters=UserFilter, order=UserOrder)
class UserType:
id: auto
name: auto
email: auto
@strawberry.type
class Query:
users: list[UserType] = orm.field()
You can also supply filter and order types explicitly:
@strawberry.type
class Query:
users: list[UserType] = orm.field(filters=UserFilter, order=UserOrder)
Explicit Resolvers
For custom scoping, join logic, or backend-specific behavior, return a backend query object from a normal Strawberry resolver:
@strawberry.type
class Query:
@strawberry.field
def active_users(self, info: strawberry.types.Info) -> list[UserType]:
return select(User).where(User.is_active.is_(True)) # SQLAlchemy
# return User.objects.filter(is_active=True) # Django
# return User.filter(is_active=True) # Tortoise
This works with the optimizer extension and with type-level get_queryset hooks.
Mutations
Write plain @strawberry.mutation resolvers and use strawberry-orm for the generated input types:
CreatePostInput = orm.input(Post, include=["title", "body", "author_id"])
@strawberry.input
class UpdatePostInput:
id: int
title: str | None = strawberry.UNSET
body: str | None = strawberry.UNSET
@strawberry.type
class Mutation:
@strawberry.mutation
def create_post(self, info: strawberry.types.Info, input: CreatePostInput) -> PostType:
post = Post(title=input.title, body=input.body, author_id=input.author_id)
...
return post
@strawberry.mutation
def update_post(self, info: strawberry.types.Info, input: UpdatePostInput) -> PostType | None:
...
Related List Inputs (orm.ref)
orm.ref(...) generates a @oneOf input for managing related lists:
CreateTagInput = orm.input(Tag, include=["name"])
@strawberry.input
class UpdateTagInput:
id: strawberry.ID
name: str
TagRef = orm.ref(Tag, create=CreateTagInput, update=UpdateTagInput, delete=True)
Each ref in the list can be one of:
{ id: "1" }— link an existing row{ create: { ... } }— create a related row inline{ update: { id: "...", ... } }— update an existing related row{ delete: { id: "..." } }— unlink (or delete ifhard_delete_refs=True)
Apply ref operations in a mutation:
@strawberry.mutation
def set_post_tags(self, info: strawberry.types.Info, post_id: int, tags: list[TagRef]) -> PostType | None:
post = ...
orm.apply_ref_list(post, "tags", tags, info)
return post
mutation {
setPostTags(postId: 1, tags: [
{ id: "2" }
{ update: { id: "1", name: "python3" } }
{ create: { name: "new-tag" } }
{ delete: { id: "3" } }
]) {
id
tags { id name }
}
}
apply_ref_list supports mode="replace" (default, replaces the entire list) and mode="patch" (only touches mentioned items). An optional authorize callback receives (action, model, obj_id, info) and returns bool.
Query Optimization
Add the optimizer extension to your schema:
schema = strawberry.Schema(
query=Query,
mutation=Mutation,
extensions=[orm.optimizer_extension()],
)
The optimizer:
- executes backend query objects returned by your resolvers
- eager-loads relations based on the GraphQL selection set
- applies field-level hints registered through
orm.field(...) - honors type-level
get_querysethooks
Field Hints
Inside @orm.type(...), orm.field(...) attaches optimizer metadata:
@orm.type(Post)
class PostType:
id: auto
title: auto
tags: list[TagType] = orm.field(load=["author"])
body: auto = orm.field(only=["id", "title", "body"])
| Argument | Meaning |
|---|---|
load=[...] |
Extra eager-load paths to apply. |
load=callable |
A callable that customises the queryset for a related field (see below). |
only=[...] |
Restrict loaded columns. |
compute={...} |
Register computed-column hints for the optimizer store. |
disable_optimization=True |
Skip optimization for that field. |
description="..." |
Forward a field description to Strawberry. |
Custom Querysets on Related Fields (load=callable)
When load is a callable instead of a list, it receives the default queryset for the related model and returns a modified queryset. This lets you filter, reorder, or limit related objects from the parent level:
@orm.type(User)
class UserType:
id: auto
name: auto
posts: list[PostType] = orm.field(
load=lambda qs: qs.filter(is_published=True)
)
How each backend applies the callable:
- Django — wraps the relation in a
Prefetchobject with the custom queryset. - SQLAlchemy — extracts
WHEREcriteria from the modifiedselect()and applies them viarelationship.and_(...). - Tortoise — performs a separate batch query filtered by parent IDs and assigns results back to each parent instance.
This composes with type-level get_queryset. If the related type defines get_queryset and the field has a load callable, both are applied (type-level first, then the field-level callable):
@orm.type(Post)
class PublishedPostType:
id: auto
title: auto
@classmethod
def get_queryset(cls, qs, info):
return qs.filter(is_published=True)
@orm.type(User)
class UserType:
id: auto
name: auto
posts: list[PublishedPostType] = orm.field(
load=lambda qs: qs.order_by("-created_at")
)
The optimizer handles batching, so this avoids N+1 queries even with custom filtering.
Field Permissions
Use make_field(...) to attach Strawberry permission classes to a generated field:
from strawberry_orm import make_field
@orm.type(User)
class UserType:
id: auto
name: auto
email: auto = make_field(permission_classes=[IsAuthenticated])
Security
strawberry-orm has safety-focused defaults, but you still need to make deliberate schema choices.
Defaults:
orm.input(),orm.filter(), andorm.order()exclude sensitive-looking fields such aspassword_hash,api_key,role, andis_adminby default- String regex filters are disabled by default
- Filter depth, branch count, and
inListsize are capped by default orm.ref(..., delete=True)unlinks by default; hard deletes requirehard_delete_refs=True
Caveats:
orm.type()does not auto-hide sensitive output fields. It warns by default, but you must still useexclude=[...]or permission classes to protect them.- List queries are unbounded unless you set
default_query_limit=... apply_ref_list()only enforces authorization if you provide anauthorizecallback- GraphQL introspection, auth, and query-complexity limits are still your application's responsibility
A production-oriented configuration:
orm = StrawberryORM(
"sqlalchemy",
dialect="postgresql",
session_getter=lambda info: info.context["session"],
default_query_limit=100,
max_filter_depth=8,
max_filter_branches=25,
max_in_list_size=200,
)
Public Exports
Top-level exports from strawberry_orm:
StrawberryORM, auto, make_field, make_ref_type, Ordering, FieldDefinition, FieldHints, OptimizerExtension, OptimizerStore, UNSET, and the built-in lookup input classes from strawberry_orm.filters.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file strawberry_orm-0.1.1.tar.gz.
File metadata
- Download URL: strawberry_orm-0.1.1.tar.gz
- Upload date:
- Size: 33.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5181303eb6f43881b7c33c3b3849261c82734704b2d50f2f1af5a1de0e95c44b
|
|
| MD5 |
1f6c116ea4a20308a543e735b37da7a7
|
|
| BLAKE2b-256 |
6aa169cf1bded7e4deec8469a5b7dfb392794fd63ebe35fc593a7ee4eb053635
|
Provenance
The following attestation bundles were made for strawberry_orm-0.1.1.tar.gz:
Publisher:
publish.yml on strawberry-graphql/strawberry-orm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
strawberry_orm-0.1.1.tar.gz -
Subject digest:
5181303eb6f43881b7c33c3b3849261c82734704b2d50f2f1af5a1de0e95c44b - Sigstore transparency entry: 1107848157
- Sigstore integration time:
-
Permalink:
strawberry-graphql/strawberry-orm@05504fe7119296cdb5d6c75cb0a37b1a9637e15e -
Branch / Tag:
refs/heads/main - Owner: https://github.com/strawberry-graphql
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@05504fe7119296cdb5d6c75cb0a37b1a9637e15e -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file strawberry_orm-0.1.1-py3-none-any.whl.
File metadata
- Download URL: strawberry_orm-0.1.1-py3-none-any.whl
- Upload date:
- Size: 41.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
36e01d4b96b1dd118ace53470fddb0b392f6781bcb57fdce76a7e80c0a8252e3
|
|
| MD5 |
3cfa5d89903404944377950c907b233c
|
|
| BLAKE2b-256 |
17bbf9c95bc3445f25fa6176516d6e52a9aa17f5f27c88fbb6dbd5e9b31dabe7
|
Provenance
The following attestation bundles were made for strawberry_orm-0.1.1-py3-none-any.whl:
Publisher:
publish.yml on strawberry-graphql/strawberry-orm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
strawberry_orm-0.1.1-py3-none-any.whl -
Subject digest:
36e01d4b96b1dd118ace53470fddb0b392f6781bcb57fdce76a7e80c0a8252e3 - Sigstore transparency entry: 1107848164
- Sigstore integration time:
-
Permalink:
strawberry-graphql/strawberry-orm@05504fe7119296cdb5d6c75cb0a37b1a9637e15e -
Branch / Tag:
refs/heads/main - Owner: https://github.com/strawberry-graphql
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@05504fe7119296cdb5d6c75cb0a37b1a9637e15e -
Trigger Event:
workflow_dispatch
-
Statement type: