Files
prompts/docs/skills/async-fastapi-sqlmodel/references/sqlmodel.md
T
2026-07-30 01:28:39 -05:00

5.2 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 engine and factory primitives as the runtime base: create_async_engine and async_sessionmaker. For SQLModel applications, use SQLModel's AsyncSession wrapper so its typed exec() API remains available.
  • 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.

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 async_sessionmaker, create_async_engine
from sqlmodel import select
from sqlmodel.ext.asyncio.session import AsyncSession

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, and SQLModel's AsyncSession adds typed exec() results while retaining SQLAlchemy's async lifecycle and transaction behavior. Import AsyncSession from sqlmodel.ext.asyncio.session when working with SQLModel models; use SQLAlchemy's AsyncSession only when the code intentionally has no SQLModel dependency.


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 Session example 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.