Skip to main content

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-orm is 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

  1. Create a backend instance.
  2. Generate Strawberry types from ORM models with @orm.type(...).
  3. Generate filter/order types from the same models.
  4. Expose list fields with orm.field().
  5. 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 filter and order arguments automatically because UserType carries 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.session
  • info.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=UserFilter
  • order=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 if hard_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_queryset hooks

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 Prefetch object with the custom queryset.
  • SQLAlchemy — extracts WHERE criteria from the modified select() and applies them via relationship.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(), and orm.order() exclude sensitive-looking fields such as password_hash, api_key, role, and is_admin by default
  • String regex filters are disabled by default
  • Filter depth, branch count, and inList size are capped by default
  • orm.ref(..., delete=True) unlinks by default; hard deletes require hard_delete_refs=True

Caveats:

  • orm.type() does not auto-hide sensitive output fields. It warns by default, but you must still use exclude=[...] 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 an authorize callback
  • 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

strawberry_orm-0.1.1.tar.gz (33.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

strawberry_orm-0.1.1-py3-none-any.whl (41.8 kB view details)

Uploaded Python 3

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

Hashes for strawberry_orm-0.1.1.tar.gz
Algorithm Hash digest
SHA256 5181303eb6f43881b7c33c3b3849261c82734704b2d50f2f1af5a1de0e95c44b
MD5 1f6c116ea4a20308a543e735b37da7a7
BLAKE2b-256 6aa169cf1bded7e4deec8469a5b7dfb392794fd63ebe35fc593a7ee4eb053635

See more details on using hashes here.

Provenance

The following attestation bundles were made for strawberry_orm-0.1.1.tar.gz:

Publisher: publish.yml on strawberry-graphql/strawberry-orm

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for strawberry_orm-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 36e01d4b96b1dd118ace53470fddb0b392f6781bcb57fdce76a7e80c0a8252e3
MD5 3cfa5d89903404944377950c907b233c
BLAKE2b-256 17bbf9c95bc3445f25fa6176516d6e52a9aa17f5f27c88fbb6dbd5e9b31dabe7

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page