# SQLModel vs SQLAlchemy: Schema Definition and Relationship Handling in FastAPI

> Compare SQLModel vs SQLAlchemy for FastAPI database modeling. Discover key differences in schema definition and relationship handling, simplifying your Python ORM workflow.

- Repository: [Sebastián Ramírez/fastapi](https://github.com/tiangolo/fastapi)
- Tags: deep-dive
- Published: 2026-02-18

---

**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`](https://github.com/tiangolo/fastapi/blob/main/docs_src/sql_databases/tutorial001_py310.py), the FastAPI documentation demonstrates this unified pattern:

```python
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:

```python
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`](https://github.com/tiangolo/fastapi/blob/main/docs_src/sql_databases/tutorial002_py310.py), SQLModel relationships use the familiar SQLAlchemy pattern:

```python
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`:

```python
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`](https://github.com/tiangolo/fastapi/blob/main/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` (where `Base = declarative_base()`)

Migration scripts and Alembic configuration remain identical between the two approaches. You configure Alembic's [`env.py`](https://github.com/tiangolo/fastapi/blob/main/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_safe` flag in [`fastapi/encoders.py`](https://github.com/tiangolo/fastapi/blob/main/fastapi/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.