Skip to content

ORM Integration

中文版

When your ORM already knows the relationships between tables, you can avoid duplicating relationship declarations. build_relationship() inspects ORM metadata and auto-generates Relationship definitions and DataLoader functions.

Supported ORMs

ORM Import Status
SQLAlchemy pydantic_resolve.integration.sqlalchemy Full support
Django pydantic_resolve.integration.django Full support
Tortoise ORM pydantic_resolve.integration.tortoise Full support

Installation

pip install pydantic-resolve[sqlalchemy]   # SQLAlchemy
pip install pydantic-resolve[django]       # Django
pip install pydantic-resolve[tortoise]     # Tortoise ORM

How It Works

The integration follows three steps:

  1. Define Pydantic DTOs that mirror your ORM models.
  2. Map DTOs to ORM models via Mapping.
  3. Call build_relationship() to generate Entity objects with loaders.
flowchart LR
    DTO["Pydantic DTOs"] --> Mapping
    ORM["ORM Models"] --> Mapping
    Mapping --> build_relationship
    build_relationship --> Entities["list[Entity]<br/>with auto-generated loaders"]
    Entities --> ErDiagram

SQLAlchemy Example

1. Define ORM Models

from sqlalchemy import ForeignKey, Integer, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship


class Base(DeclarativeBase):
    pass


class UserORM(Base):
    __tablename__ = "users"
    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    name: Mapped[str] = mapped_column(String)
    posts: Mapped[list["PostORM"]] = relationship(back_populates="author")


class PostORM(Base):
    __tablename__ = "posts"
    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    title: Mapped[str] = mapped_column(String)
    author_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
    author: Mapped["UserORM"] = relationship(back_populates="posts")
    comments: Mapped[list["CommentORM"]] = relationship(back_populates="post")


class CommentORM(Base):
    __tablename__ = "comments"
    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    content: Mapped[str] = mapped_column(String)
    post_id: Mapped[int] = mapped_column(ForeignKey("posts.id"))
    post: Mapped["PostORM"] = relationship(back_populates="comments")

2. Define Pydantic DTOs

DTOs must enable from_attributes:

Why: Generated loaders query the database through the ORM and return ORM instances. Pydantic's model_validate needs from_attributes=True to convert those instances into DTOs. Additionally, the _query_meta optimization uses DTO field names to generate load_only clauses — only the columns the DTO actually declares are fetched from the database. Without from_attributes, the conversion step would fail even though the query is already optimized.

from pydantic import BaseModel, ConfigDict


class UserDTO(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    name: str


class PostDTO(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    title: str
    author_id: int


class CommentDTO(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    content: str
    post_id: int

3. Build Relationships

from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from pydantic_resolve import ErDiagram, config_global_resolver
from pydantic_resolve.integration.mapping import Mapping
from pydantic_resolve.integration.sqlalchemy import build_relationship

engine = create_async_engine("sqlite+aiosqlite:///blog.db")
session_factory = async_sessionmaker(engine, expire_on_commit=False)

entities = build_relationship(
    mappings=[
        Mapping(entity=UserDTO, orm=UserORM),
        Mapping(entity=PostDTO, orm=PostORM),
        Mapping(entity=CommentDTO, orm=CommentORM),
    ],
    session_factory=session_factory,
)

diagram = ErDiagram(entities=entities)
AutoLoad = diagram.create_auto_load()
config_global_resolver(diagram)

4. Use in Response Models

from typing import Annotated, Optional


class CommentView(CommentDTO):
    pass


class PostView(PostDTO):
    author: Annotated[Optional[UserDTO], AutoLoad()] = None
    comments: Annotated[list[CommentView], AutoLoad()] = []


class UserView(UserDTO):
    posts: Annotated[list[PostView], AutoLoad()] = []

Mapping Configuration

from pydantic_resolve.integration.mapping import Mapping

Mapping(
    entity=PostDTO,           # Pydantic model
    orm=PostORM,              # ORM model
    filters=[PostORM.active == True],  # Optional per-target filters
)
Parameter Type Description
entity type Pydantic DTO class
orm type ORM model class
filters list \| None Per-target ORM filter expressions

build_relationship() Parameters

SQLAlchemy

from pydantic_resolve.integration.sqlalchemy import build_relationship

entities = build_relationship(
    mappings=[...],
    session_factory=async_session_factory,
    default_filter=lambda orm_kls: [orm_kls.active == True],
)
Parameter Type Description
mappings list[Mapping] DTO to ORM mappings
session_factory Callable Returns an async AsyncSession
default_filter Callable \| None Receives target ORM class, returns filter expressions

Django

from pydantic_resolve.integration.django import build_relationship

entities = build_relationship(
    mappings=[...],
    using='default',  # database alias or callable returning one
    default_filter=lambda orm_kls: [orm_kls.active == True],
)
Parameter Type Description
mappings list[Mapping] DTO to ORM mappings
using Any \| None Database alias for QuerySet.using()
default_filter Callable \| None Receives target ORM class, returns filter expressions

Tortoise ORM

from pydantic_resolve.integration.tortoise import build_relationship

entities = build_relationship(
    mappings=[...],
)

Supported Relationship Types

Relationship SQLAlchemy Django Tortoise
Many-to-One yes yes yes
One-to-Many yes yes yes
One-to-One (forward) yes yes yes
Reverse One-to-One yes yes yes
Many-to-Many yes yes yes

Generated Loader Behavior

The auto-generated loaders:

  • Use load_only (SQLAlchemy) / only (Django) to select only the columns needed by the DTO
  • Apply per-mapping or default filters
  • Convert ORM rows to DTO via model_validate
  • Handle both sync and async sessions

DTO Validation

build_relationship validates that all required DTO fields exist as scalar columns on the ORM model:

class TaskDTO(BaseModel):
    id: int
    name: str
    priority: str  # Not in ORM

# ValueError: Required DTO fields not found in ORM scalar fields
# for mapping TaskDTO -> TaskORM: priority

Merging with Existing ERD

Use ErDiagram.add_relationship() to merge ORM-generated entities with hand-written ones:

from pydantic_resolve import base_entity

BaseEntity = base_entity()

class UserEntity(BaseModel, BaseEntity):
    id: int
    name: str

# Hand-written diagram
diagram = BaseEntity.get_diagram()

# ORM-generated entities
sa_entities = build_relationship(
    mappings=[Mapping(entity=TaskDTO, orm=TaskORM)],
    session_factory=session_factory,
)

# Merge
merged_diagram = diagram.add_relationship(sa_entities)
AutoLoad = merged_diagram.create_auto_load()
config_global_resolver(merged_diagram)

Merge rules for entities with the same kls:

  • relationships: merged by name (raises ValueError on duplicate)
  • queries: merged by method name (raises ValueError on duplicate)
  • mutations: merged by method name (raises ValueError on duplicate)

Entities with a new kls are appended.

Limitations

  • SQLAlchemy: Composite foreign keys are not supported (raises NotImplementedError)
  • SQLAlchemy: MANYTOMANY without explicit secondary table is not supported
  • Unmapped ORM targets are skipped with a warning
  • Generated loaders do not support custom transformation logic — use hand-written loaders for complex cases

When to Use ORM Integration

ORM integration is a good fit when:

  • Your ORM metadata is stable and already defines all relationships
  • You have many entities and want to avoid hand-writing loaders
  • You want a single source of truth for relationships (the ORM)

Keep using hand-written loaders when:

  • You need custom transformation or filtering logic
  • Data comes from multiple sources (not just one database)
  • The ORM does not match your API response structure closely

Next

Continue to FastAPI Integration to see how to use the resolver in FastAPI endpoints.