# SQLModel Adoption and Boundaries !!! info "Primary sources" - [SQLModel documentation](https://sqlmodel.tiangolo.com/) - [SQLModel FastAPI session dependency tutorial](https://sqlmodel.tiangolo.com/tutorial/fastapi/session-with-dependency/) - [SQLModel release notes](https://sqlmodel.tiangolo.com/release-notes/) - [SQLAlchemy asyncio extension](https://docs.sqlalchemy.org/en/21/orm/extensions/asyncio.html) ??? 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. --- ## Recommended Patterns ### 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. ```python 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 ```python 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.