4.9 KiB
SQLModel-First Modeling and Async Boundaries
!!! info "Primary sources" - SQLModel documentation - SQLModel features - SQLModel advanced guide - SQLModel FastAPI session dependency tutorial - SQLModel release notes - SQLAlchemy asyncio extension
??? abstract "Decision metadata" - Status: adopted - Decision level: mandatory - Applies to: api-runtime, workers, tests - Last reviewed: 2026-07-26
Purpose
Define SQLModel as the primary model layer for async FastAPI applications and explain how it composes with SQLAlchemy's async runtime.
SQLModel is designed for FastAPI, built on Pydantic and SQLAlchemy, and intended to minimize duplication while preserving the capabilities of both. Async engine, session, transaction, and loading behavior still follow SQLAlchemy's asyncio contract.
Scope and Non-Goals
- In scope: table models, API data models, SQLAlchemy interoperability, async session usage, and exception criteria.
- Out of scope: replacing SQLAlchemy's async runtime primitives or claiming that synchronous tutorial examples are async patterns.
Rules
- Default to SQLModel for new table models and API data models.
- Keep SQLAlchemy async primitives as the runtime base:
create_async_engine,async_sessionmaker, andAsyncSession. - Keep transaction and session ownership policies identical whether models are SQLAlchemy Declarative or SQLModel.
- Use SQLModel inheritance to share validated fields while keeping table, create, update, and public contracts distinct where their semantics differ.
- Use SQLAlchemy declarative models only for a concrete unsupported mapping or third-party constraint; document the reason.
- Use SQLAlchemy relationship loading options explicitly on async paths.
Recommended Patterns
Pattern A: Data model split for API boundaries
Use distinct models for persistence and external contracts.
from sqlmodel import Field, SQLModel
class UserBase(SQLModel):
email: str
display_name: str
class User(UserBase, table=True):
id: int | None = Field(default=None, primary_key=True)
class UserCreate(UserBase):
pass
class UserRead(UserBase):
id: int
Pattern B: Keep SQLModel models with the async runtime
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlmodel import select
engine = create_async_engine(settings.database_url, pool_pre_ping=True)
session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with session_factory() as session:
users = (await session.scalars(select(User))).all()
sqlmodel.select() keeps SQLModel's typing-oriented statement construction, while AsyncSession.scalars() and the surrounding lifecycle come from SQLAlchemy.
Interoperability Notes
- A SQLModel table model is a SQLAlchemy model and can participate in SQLAlchemy relationships, statements, loader options, and sessions.
- A SQLModel model is also a Pydantic model; non-table models are useful for request and response contracts.
- SQLModel's official FastAPI dependency tutorial currently uses synchronous
Session; translate the ownership pattern, not the concrete session type, for async applications. - SQLModel's advanced guide still lists dedicated async documentation as future work, so use SQLAlchemy's asyncio documentation as the authority for runtime mechanics.
- Prefer one query style per module to reduce cognitive overhead.
- Keep loader strategies explicit in async paths to avoid implicit I/O surprises.
Anti-Patterns
- Treating SQLModel as an alternative to SQLAlchemy rather than a layer built on it.
- Copying a synchronous
Sessionexample into an async request path. - Constructing sessions in handlers instead of using the application session factory.
- Mixing multiple query/session idioms within the same module without clear conventions.
Operational Checks
- New model modules are SQLModel-first; exceptions state the unsupported need or constraint.
- Session/transaction ownership remains consistent across both model styles.
- Table, create, update, and public models share fields intentionally without exposing persistence-only data.
Testing Checks
- Module-level tests verify CRUD semantics for SQLModel models through
AsyncSession. - API tests verify response/request model behavior for SQLModel-based endpoints.
- Relationship tests verify async loader strategies do not depend on implicit I/O.
Version Checks
- Verify installed SQLModel, SQLAlchemy, and Pydantic versions together when using newly added typing or ORM features.