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
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
.
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
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
.