Files
prompts/docs/skills/async-fastapi-sqlmodel/references/sqlmodel.md
T

3.9 KiB

SQLModel Adoption and Boundaries

!!! info "Primary sources" - SQLModel documentation - SQLModel FastAPI session dependency tutorial - SQLModel release notes - SQLAlchemy asyncio extension

??? abstract "Decision metadata" - Status: adopted - Decision level: advisory - Applies to: api-runtime, workers, tests - Last reviewed: 2026-06-26


Purpose

Define when and how to use SQLModel in an async FastAPI + SQLAlchemy modernization effort.

The goal is pragmatic adoption: use SQLModel where it reduces model duplication and improves typing ergonomics, without disrupting established async engine/session lifecycle rules.


Scope and Non-Goals

  • In scope: model-layer decisions, integration boundaries, phased adoption strategy.
  • Out of scope: full framework rewrites and all-at-once model migration.

Rules

  • Keep SQLAlchemy async primitives as the runtime base: create_async_engine, async_sessionmaker, and AsyncSession.
  • Prefer SQLModel for new domain modules where table models and API schemas would otherwise be duplicated.
  • Migrate by bounded module or feature area; do not force whole-repo conversion in one phase.
  • Keep transaction and session ownership policies identical whether models are SQLAlchemy Declarative or SQLModel.
  • Document explicit reasons when SQLModel is deferred for a module.

Pattern A: Bounded module adoption

  • Choose one feature slice (for example, billing, projects, or auth profile data).
  • Introduce SQLModel models for that slice only.
  • Keep unchanged modules on existing SQLAlchemy models until a dedicated migration phase.

Pattern B: 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 C: Keep async lifecycle unchanged

from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine

engine = create_async_engine(settings.database_url, pool_pre_ping=True)
session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)

Interoperability Notes

  • SQLModel is designed as a thin layer over SQLAlchemy and Pydantic, so mixed codebases are expected during migration.
  • 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 adoption as equivalent to async-session modernization.
  • Rewriting all models at once without rollback checkpoints.
  • Introducing SQLModel in handlers while keeping old global/shared session patterns.
  • Mixing multiple query/session idioms within the same module without clear conventions.

Operational Checks

  • Modernized module documents whether it is SQLModel-first or SQLAlchemy-only.
  • Session/transaction ownership remains consistent across both model styles.
  • New model modules use explicit API boundary models where needed.

Testing Checks

  • Module-level tests verify CRUD semantics for adopted SQLModel models.
  • API tests verify response/request model behavior for SQLModel-based endpoints.
  • Regression tests confirm unchanged modules continue to function during phased rollout.

Migration Notes

  • Start with low-risk bounded domains.
  • Expand only after validation of session lifecycle, transaction behavior, and endpoint correctness.
  • Maintain a tracked backlog of deferred modules with rationale and planned phase.