SQLModel vs SQLAlchemy: Schema Definition and Relationship Handling in FastAPI
SQLModel combines SQLAlchemy's ORM engine with Pydantic's validation in a single class, eliminating boilerplate and enabling automatic OpenAPI schema generation, while raw SQLAlchemy requires separate Pydantic schemas and manual JSON handling via the sqlalchemy_safe flag but offers advanced mapping flexibility.
When building database-driven applications with FastAPI, choosing between SQLModel vs SQLAlchemy determines how you define schemas, handle relationships, and serialize responses. While SQLModel is a thin wrapper around SQLAlchemy created by the same author as the FastAPI framework (tiangolo/fastapi), the two libraries expose fundamentally different developer experiences. This analysis examines their architectural approaches using actual source files from the FastAPI repository.
Schema Definition Approaches
SQLModel's Pydantic-Integrated Models
SQLModel models inherit from SQLModel and declare fields using sqlmodel.Field. You mark a class as a database table by setting table=True. Because SQLModel extends pydantic.BaseModel, the same class serves simultaneously as an ORM model, request validation schema, and response serialization model.
In docs_src/sql_databases/tutorial001_py310.py, the FastAPI documentation demonstrates this unified pattern:
from sqlmodel import Field, SQLModel
class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str = Field(index=True)
age: int | None = Field(default=None, index=True)
secret_name: str
This single class definition eliminates the need for separate Pydantic schemas. FastAPI automatically generates OpenAPI documentation from the type annotations, and the model instance can be passed directly to database sessions and JSON responses without conversion.
SQLAlchemy's Traditional ORM Mapping
Raw SQLAlchemy requires models to inherit from declarative_base(). Columns are defined with Column objects from the sqlalchemy module. The resulting class is purely an ORM mapping with no built-in validation or serialization capabilities.
The equivalent SQLAlchemy implementation requires explicit column definitions:
from sqlalchemy import Column, Integer, String, create_engine, ForeignKey
from sqlalchemy.orm import declarative_base, sessionmaker
Base = declarative_base()
class Hero(Base):
__tablename__ = "hero"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, index=True)
age = Column(Integer, index=True, nullable=True)
secret_name = Column(String, nullable=False)
When using raw SQLAlchemy with FastAPI, you must create separate Pydantic schemas for request validation and response serialization. This separation provides flexibility for advanced mapping strategies but introduces significant boilerplate.
Handling Relationships
Relationship Syntax Compatibility
Both libraries use identical underlying mechanisms for database relationships because SQLModel re-exports SQLAlchemy's relationship function as Relationship. The syntax for defining one-to-many or many-to-one relationships remains the same in both approaches.
In docs_src/sql_databases/tutorial002_py310.py, SQLModel relationships use the familiar SQLAlchemy pattern:
from typing import List, Optional
from sqlmodel import Relationship
class Team(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
heroes: List["Hero"] = Relationship(back_populates="team")
class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
team_id: Optional[int] = Field(default=None, foreign_key="team.id")
team: Optional[Team] = Relationship(back_populates="heroes")
The equivalent raw SQLAlchemy implementation uses the same relationship import from sqlalchemy.orm:
from sqlalchemy.orm import relationship
class Team(Base):
__tablename__ = "team"
id = Column(Integer, primary_key=True)
name = Column(String)
heroes = relationship("Hero", back_populates="team")
class Hero(Base):
__tablename__ = "hero"
id = Column(Integer, primary_key=True)
name = Column(String)
team_id = Column(Integer, ForeignKey("team.id"))
team = relationship("Team", back_populates="heroes")
OpenAPI Schema Generation Differences
The critical distinction emerges when FastAPI generates OpenAPI documentation. SQLModel exposes relationship fields in the generated schema because they are Pydantic fields. When you set response_model=Hero in a FastAPI endpoint, the team relationship appears in the API documentation automatically.
With raw SQLAlchemy, relationship attributes are not Pydantic fields. They exist only as ORM constructs. To include related objects in API responses, you must explicitly define nested Pydantic schemas and use response_model to reference them. FastAPI cannot introspect raw SQLAlchemy relationships for OpenAPI generation.
JSON Serialization and FastAPI Integration
FastAPI's internal JSON encoder (jsonable_encoder) handles the two libraries differently. In fastapi/encoders.py (lines 193-199), the framework includes a sqlalchemy_safe flag that strips private _sa_* attributes from SQLAlchemy objects before serialization. This prevents internal SQLAlchemy state from leaking into JSON responses.
When using SQLModel, this extra step becomes unnecessary. Because SQLModel inherits from Pydantic's BaseModel, it conforms to the standard Pydantic contract that FastAPI's encoder handles natively. You can return SQLModel instances directly from endpoints without worrying about internal attribute leakage or manual conversion.
Migration Workflow Comparison
Both libraries use Alembic for database migrations because SQLModel is built directly on SQLAlchemy's engine. The only difference lies in metadata access:
- SQLModel: Access metadata via
SQLModel.metadata(the library maintains a global metadata object) - SQLAlchemy: Access metadata via
Base.metadata(whereBase = declarative_base())
Migration scripts and Alembic configuration remain identical between the two approaches. You configure Alembic's env.py to point to the appropriate metadata object, and all migration commands (alembic revision, alembic upgrade) work the same way.
Summary
- SQLModel unifies ORM mapping and Pydantic validation in a single class, eliminating boilerplate and enabling automatic OpenAPI schema generation for relationships.
- SQLAlchemy requires separate Pydantic schemas and manual JSON handling via the
sqlalchemy_safeflag infastapi/encoders.py, but offers advanced mapping flexibility for complex domain models. - Both libraries use identical relationship syntax and Alembic migration workflows, differing only in how they expose metadata and handle serialization.
- FastAPI's source code explicitly handles raw SQLAlchemy objects differently than Pydantic-based models, demonstrating the framework's optimization for SQLModel's integrated approach.
Frequently Asked Questions
Can I use SQLModel and SQLAlchemy together in the same FastAPI project?
Yes, you can mix both libraries because SQLModel is a thin wrapper around SQLAlchemy's core engine. You can define simple CRUD models with SQLModel while using raw SQLAlchemy for complex legacy mappings or advanced features like hybrid properties. Both will share the same database connection pool and Alembic migration history as long as they reference the same metadata object.
Does SQLModel support all SQLAlchemy features?
SQLModel supports the vast majority of common SQLAlchemy ORM features including relationships, indexes, constraints, and custom column types. However, advanced features like complex inheritance mappings, mapper configuration events, or custom instrumentation may require dropping down to raw SQLAlchemy. Since SQLModel classes are ultimately SQLAlchemy models under the hood, you can access the underlying SQLAlchemy API when needed.
Which should I choose for a new FastAPI project?
Choose SQLModel if you want rapid development with minimal boilerplate and your API schemas closely match your database tables. It is ideal for greenfield projects where type safety and automatic documentation are priorities. Choose raw SQLAlchemy if you need complex domain models that differ significantly from your API contracts, require advanced ORM features, or are migrating an existing SQLAlchemy codebase to FastAPI.
How do relationships affect performance in SQLModel vs SQLAlchemy?
Performance is identical because SQLModel uses SQLAlchemy's underlying relationship machinery. Both libraries generate the same SQL queries and use the same lazy-loading or eager-loading strategies. The difference lies in serialization performance: SQLModel relationships are Pydantic fields that serialize automatically, while raw SQLAlchemy relationships require manual conversion to dictionaries or Pydantic models before JSON serialization. For high-performance APIs, use selectinload or joinedload strategies in both libraries to avoid N+1 queries.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →