SQL query to create a nested pydantic structure

Question:

While working with SQLAlchemy 2.x and FastAPI, I ran into the problem of creating a nested structure for pydantic serialization.

The specific essence of the problem lies in the implementation of the sql query. The fact is that the received data comes as a single tuple and pydantic does not see a nested structure in it.

i.e. i want to get following response json structure using many to one relationship

{
  "id": 0,
  "username": "string",
  "avatar_url": "string",
  "create_at": "2023-03-30T10:56:03.625Z",
  "is_active": true,
  "user_role": {
    "name": "string",
    "color": "string"
  }
}

how to change the database query code to get this?

Parts of my code:

crud.py

# this code does not work correctly for the implementation of the current task
async def get_current_user(user_id: int, session: AsyncSession):
    result = await session.execute(
        select(
            Users.id, Users.username, Users.avatar_url, Users.create_at, Users.is_active,
            UsersRole.name, UsersRole.color
        )
        .join(Users)
        .where(Users.id == user_id)
    )
    return result.first()

models.py

class Base(DeclarativeBase):
    pass


class Users(Base):
    __tablename__ = "users"

    id: Mapped[int] = mapped_column(primary_key=True)
    username: Mapped[str] = mapped_column(String(60), unique=True, nullable=False)
    email: Mapped[str] = mapped_column(unique=True, nullable=False)
    password: Mapped[str] = mapped_column(nullable=False)
    avatar_url: Mapped[str] = mapped_column(nullable=True)
    create_at: Mapped[datetime] = mapped_column(default=datetime.utcnow())
    is_active: Mapped[bool] = mapped_column(default=True)

    role_id: Mapped[int] = mapped_column(ForeignKey("users_role.id"), nullable=False)
    role: Mapped["UsersRole"] = relationship()


class UsersRole(Base):
    __tablename__ = "users_role"

    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(nullable=False)
    color: Mapped[str] = mapped_column(String(7), nullable=False)

schemas.py

class ResponseRoleUser(BaseModel):
    name: str
    color: str


class ResponseCurrentUser(BaseModel):
    id: int
    username: str
    avatar_url: str | None
    create_at: datetime
    is_active: bool
    role: ResponseRoleUser

    class Config:
        orm_mode = True

endpoint.py

@router.get("/me", response_model=ResponseCurrentUser)
async def current_data_user(
    ...
    session: Annotated[AsyncSession, Depends(get_async_session)]
):
    # for example
    user = await get_current_user(id, session)
    return user

Tried experimenting with relationship() but it didn’t work

Asked By: MARVIIN

||

Answers:

By listing all the database columns individually, SQLAlchemy will return a Row object instead of a Users instance, which won’t map cleanly to your Pydantic models. If you instead rewrite to query the ORM classes directly:

from sqlalchemy.orm import joinedload

async def get_current_user(user_id: int, session: AsyncSession):
    result = await session.execute(
        select(Users)
        .where(Users.id == user_id)
        .options(joinedload(Users.role))
    )
    return result.first()

Note that using joinedload tells SQLAlchemy to join in the users_role table and populate the Users.role relationship, rather than lazily loading it, which doesn’t work when using async SQLAlchemy (since all IO operations need to be awaited).

Returning Users objects will allow pydantic’s ORM mode to properly convert to ResponseCurrentUser.

Answered By: M.O.