Skip to content

FastAPI 0.67.0 new dataclass related features conflict with some aspects of new SQLAlchemy dataclass-style model mapping #3616

@AlexanderPodorov

Description

@AlexanderPodorov

First Check

  • I added a very descriptive title to this issue.
  • I used the GitHub search to find a similar issue and didn't find it.
  • I searched the FastAPI documentation, with the integrated search.
  • I already searched in Google "How to X in FastAPI" and didn't find any information.
  • I already read and followed all the tutorial in the docs and didn't find an answer.
  • I already checked if it is not related to FastAPI but to Pydantic.
  • I already checked if it is not related to FastAPI but to Swagger UI.
  • I already checked if it is not related to FastAPI but to ReDoc.

Commit to Help

  • I commit to help with one of those options 👆

Example Code

from __future__ import annotations

from dataclasses import dataclass, field

from fastapi import Depends, FastAPI
from pydantic import BaseModel
from sqlalchemy import Column, ForeignKey, Integer, String, create_engine
from sqlalchemy.orm import Session, registry, relationship, sessionmaker
from uvicorn import run

SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db"

engine = create_engine(
    SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

mapper = registry()


@mapper.mapped
@dataclass
class User:
    __tablename__ = "users"
    __sa_dataclass_metadata_key__ = "sa"

    id: int = field(
        metadata={
            "sa": Column(Integer, primary_key=True, index=True)
        }
    )
    email: str = field(
        metadata={
            "sa": Column(String, unique=True, index=True)
        }
    )
    items: list[Item] = field(
        init=False,
        metadata={
            "sa": relationship("Item", lazy="raise")
        }
    )

    @property
    def greeting(self):
        return f"Hello {self.email}"


@mapper.mapped
@dataclass
class Item:
    __tablename__ = "items"
    __sa_dataclass_metadata_key__ = "sa"

    id: int = field(
        metadata={
            "sa": Column(Integer, primary_key=True, index=True)
        }
    )
    title: str = field(
        metadata={
            "sa": Column(String, index=True)
        }
    )
    owner_id: int = field(
        init=False,
        metadata={
            "sa": Column(Integer, ForeignKey("users.id"))
        }
    )


class UserIn(BaseModel):
    id: int
    email: str


class UserOut(BaseModel):
    id: int
    email: str
    greeting: str

    class Config:
        orm_mode = True


mapper.metadata.create_all(bind=engine)

app = FastAPI()


def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()


@app.post("/users/", response_model=UserOut)
def create_user(user: UserIn, db: Session = Depends(get_db)):
    instance = User(
        id=user.id,
        email=user.email
    )
    db.add(instance)
    db.commit()
    return instance


if __name__ == '__main__':
    run(app)

Description

Example code works if fastapi==0.66.1 and it does not work if fastapi==0.67.0.
Open a browser, hit POST /users/ endpoint and create a user

Expected behaviour: User is created and displayed successfully.
Current behavior: User is created successfully (check in DB), but is not displayed successfully due to serialization errors.

Most likely the reason is in the new dataclass related features of FastAPI 0.67.0. Since the new-style database models are dataclasses as well, therefore they are affected too. As far as I can see if an instance is the dataclass, then FastAPI makes a dict (dataclasses.asdict(res)) out of instance before doing serialization.
It has two issues: first, if a dataclass has a property, it won't be serialized; second, if a dataclass has a relationship with lazy="raise" (means we should load this relationship explicitly), it is actually accessed and caused SQLAlchemy's exception.
Technically, if we let pydantic to serialize this dataclass, we won't get any of those issues. I assume it was the case for previous versions of FastAPI.

Here are a few links:
https://docs.sqlalchemy.org/en/14/orm/mapping_styles.html#orm-declarative-dataclasses-declarative-table
https://docs.sqlalchemy.org/en/14/orm/loading_relationships.html#prevent-lazy-with-raiseload

Operating System

Windows

Operating System Details

No response

FastAPI Version

0.67.0

Python Version

3.9.6

Additional Context

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions