Getting `ResponseValidationError`
Chiggy-Playz opened this issue · 7 comments
Searched the issues but did not find anyone facing this issue. I'm trying to paginate my results from my database, using sqlalchemy ORM
Python version: 3.11.4
Error:
api-1 | INFO: Started server process [8]
api-1 | INFO: Waiting for application startup.
api-1 | INFO: Application startup complete.
api-1 | INFO: 172.18.0.1:53714 - "GET /api/servers/?query=2 HTTP/1.1" 500 Internal Server Error
api-1 | ERROR: Exception in ASGI application
api-1 | Traceback (most recent call last):
api-1 | File "/usr/local/lib/python3.11/site-packages/uvicorn/protocols/http/httptools_impl.py", line 435, in run_asgi
api-1 | result = await app( # type: ignore[func-returns-value]
api-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
api-1 | File "/usr/local/lib/python3.11/site-packages/uvicorn/middleware/proxy_headers.py", line 78, in __call__
api-1 | return await self.app(scope, receive, send)
api-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
api-1 | File "/usr/local/lib/python3.11/site-packages/fastapi/applications.py", line 289, in __call__
api-1 | await super().__call__(scope, receive, send)
api-1 | File "/usr/local/lib/python3.11/site-packages/starlette/applications.py", line 122, in __call__
api-1 | await self.middleware_stack(scope, receive, send)
api-1 | File "/usr/local/lib/python3.11/site-packages/starlette/middleware/errors.py", line 184, in __call__
api-1 | raise exc
api-1 | File "/usr/local/lib/python3.11/site-packages/starlette/middleware/errors.py", line 162, in __call__
api-1 | await self.app(scope, receive, _send)
api-1 | File "/usr/local/lib/python3.11/site-packages/starlette/middleware/exceptions.py", line 79, in __call__
api-1 | raise exc
api-1 | File "/usr/local/lib/python3.11/site-packages/starlette/middleware/exceptions.py", line 68, in __call__
api-1 | await self.app(scope, receive, sender)
api-1 | File "/usr/local/lib/python3.11/site-packages/fastapi/middleware/asyncexitstack.py", line 20, in __call__
api-1 | raise e
api-1 | File "/usr/local/lib/python3.11/site-packages/fastapi/middleware/asyncexitstack.py", line 17, in __call__
api-1 | await self.app(scope, receive, send)
api-1 | File "/usr/local/lib/python3.11/site-packages/starlette/routing.py", line 718, in __call__
api-1 | await route.handle(scope, receive, send)
api-1 | File "/usr/local/lib/python3.11/site-packages/starlette/routing.py", line 276, in handle
api-1 | await self.app(scope, receive, send)
api-1 | File "/usr/local/lib/python3.11/site-packages/starlette/routing.py", line 66, in app
api-1 | response = await func(request)
api-1 | ^^^^^^^^^^^^^^^^^^^
api-1 | File "/usr/local/lib/python3.11/site-packages/fastapi/routing.py", line 291, in app
api-1 | content = await serialize_response(
api-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^
api-1 | File "/usr/local/lib/python3.11/site-packages/fastapi/routing.py", line 154, in serialize_response
api-1 | raise ResponseValidationError(
api-1 | fastapi.exceptions.ResponseValidationError
Code:
from fastapi_pagination.ext.sqlalchemy import paginate
@router.get("/", response_model=Page[ServerResponse])
async def filter_servers(
query: str = Query(description="Query to filter servers"),
game_type: GameType | None = Query(None, description="Game type of server"),
platform: list[Platform] | None = Query(None, description="Platform(s) of server"),
official: bool | None = Query(None, description="Whether server is official"),
session: AsyncSession = Depends(get_db_session),
):
logger.info(
f"Filtering servers with query: {query}, game_type: {game_type}, platform: {platform}, official: {official}"
)
statement = select(ServerModel).where(ServerModel.name.ilike(f"%{query}%"))
if game_type:
statement = statement.where(ServerModel.game_type == game_type)
if platform:
statement = statement.where(ServerModel.platforms == platform)
if official is not None:
statement = statement.where(ServerModel.official == official)
return paginate(
session,
statement,
)
Hi @Chiggy-Playz,
Could you please show me full exception traceback? Also, it will be helpful to see definitions of ServerResponse and ServerModel.
Hey @uriyyo
I continued working on my project and have added a bit more stuff since then, so right now my models look like this:
class GameType(enum.Enum):
"""Enum for game type."""
ase = "ase"
asa = "asa"
class Platform(enum.Enum):
"""Enum for platform."""
xbox = "Xbox"
playstation = "Playstation"
pc = "PC"
unknown = "Unknown"
any = "Any"
class PlatformType(TypeDecorator):
"""Platform type for server model."""
impl = JSONB
cache_ok = True
def process_bind_param(self, value, dialect):
"""Used to convert enum to string."""
if value:
value = [platform.value for platform in value]
return value
def process_result_value(self, value, dialect):
"""Used to convert string to enum."""
if value:
value = [Platform(platform) for platform in value]
return value
class ServerModel(Base):
"""Model for server."""
__tablename__ = "servers"
__table_args__ = (
UniqueConstraint(
"game_type",
"name",
"platforms",
"official",
name="unique_server",
),
)
id: Mapped[int] = mapped_column(Integer, autoincrement=True, primary_key=True)
game_type: Mapped[GameType] = mapped_column(Enum(GameType), nullable=False)
name: Mapped[str] = mapped_column(String, nullable=False)
platforms: Mapped[list[Platform]] = mapped_column(
PlatformType,
nullable=False,
)
official: Mapped[bool] = mapped_column(Boolean, nullable=False)
map: Mapped[str] = mapped_column(String, nullable=False)
details: Mapped["ServerDetailsModel"] = relationship(
back_populates="server",
lazy="joined",
)
class ServerDetailsModel(Base):
"""Model for server_details."""
__tablename__ = "server_details"
id: Mapped[int] = mapped_column(ForeignKey("servers.id"), primary_key=True)
players: Mapped[int] = mapped_column(Integer, nullable=False)
ip: Mapped[str] = mapped_column(String, nullable=False)
port: Mapped[int] = mapped_column(Integer, nullable=False)
day: Mapped[int] = mapped_column(Integer, nullable=False)
last_seen_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
)
download_characters: Mapped[bool] = mapped_column(Boolean, nullable=False)
download_items: Mapped[bool] = mapped_column(Boolean, nullable=False)
server: Mapped[ServerModel] = relationship(back_populates="details")
And here's the response model:
class ServerResponse(BaseModel):
"""Server Response."""
id: int
game_type: GameType
name: str
platforms: list[str]
official: bool
map: str
# Commented rn because I don't know how to make this work with pagination, since the below properties are not on the sqlalchemy model directly but instead on .details attribute (ServerDetailsModel)
# players: int
# ip: str
# port: int
# day: int
# download_items: bool
# download_characters: bool
# last_seen_at: datetime
Here's my endpoint:
@router.get("/", response_model=Page[ServerResponse])
async def filter_servers(
query: str = Query(description="Query to filter servers"),
game_type: GameType = Query(None, description="Game type of server"),
platform: list[Platform] = Query(None, description="Platform(s) of server"),
official: bool = Query(None, description="Whether server is official"),
db: AsyncSession = Depends(get_db_session),
):
statement = select(ServerModel).where(
ServerModel.name.ilike(f"%{query}%"),
)
if game_type is not None:
statement = statement.where(ServerModel.game_type == game_type)
if platform is not None:
statement = statement.where(ServerModel.platforms == platform)
if official is not None:
statement = statement.where(ServerModel.official == official)
return paginate(db, statement)
Here's the full exception thats getting logged (It feels like its cut at the end but thats all there is):
2024-02-20 13:40:14.440 | INFO | logging:callHandlers:1706 - 172.18.0.1:59614 - "GET /api/servers/?query=knights HTTP/1.1" 500
2024-02-20 13:40:14.441 | ERROR | logging:callHandlers:1706 - Exception in ASGI application
Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/usr/local/lib/python3.11/multiprocessing/spawn.py", line 120, in spawn_main
exitcode = _main(fd, parent_sentinel)
File "/usr/local/lib/python3.11/multiprocessing/spawn.py", line 133, in _main
return self._bootstrap(parent_sentinel)
File "/usr/local/lib/python3.11/multiprocessing/process.py", line 314, in _bootstrap
self.run()
File "/usr/local/lib/python3.11/multiprocessing/process.py", line 108, in run
self._target(*self._args, **self._kwargs)
File "/usr/local/lib/python3.11/site-packages/uvicorn/_subprocess.py", line 76, in subprocess_started
target(sockets=sockets)
File "/usr/local/lib/python3.11/site-packages/uvicorn/server.py", line 61, in run
return asyncio.run(self.serve(sockets=sockets))
File "/usr/local/lib/python3.11/asyncio/runners.py", line 190, in run
return runner.run(main)
File "/usr/local/lib/python3.11/asyncio/runners.py", line 118, in run
return self._loop.run_until_complete(task)
> File "/usr/local/lib/python3.11/site-packages/uvicorn/protocols/http/httptools_impl.py", line 435, in run_asgi
result = await app( # type: ignore[func-returns-value]
File "/usr/local/lib/python3.11/site-packages/uvicorn/middleware/proxy_headers.py", line 78, in __call__
return await self.app(scope, receive, send)
File "/usr/local/lib/python3.11/site-packages/fastapi/applications.py", line 289, in __call__
await super().__call__(scope, receive, send)
File "/usr/local/lib/python3.11/site-packages/starlette/applications.py", line 122, in __call__
await self.middleware_stack(scope, receive, send)
File "/usr/local/lib/python3.11/site-packages/starlette/middleware/errors.py", line 184, in __call__
raise exc
File "/usr/local/lib/python3.11/site-packages/starlette/middleware/errors.py", line 162, in __call__
await self.app(scope, receive, _send)
File "/usr/local/lib/python3.11/site-packages/starlette/middleware/exceptions.py", line 79, in __call__
raise exc
File "/usr/local/lib/python3.11/site-packages/starlette/middleware/exceptions.py", line 68, in __call__
await self.app(scope, receive, sender)
File "/usr/local/lib/python3.11/site-packages/fastapi/middleware/asyncexitstack.py", line 20, in __call__
raise e
File "/usr/local/lib/python3.11/site-packages/fastapi/middleware/asyncexitstack.py", line 17, in __call__
await self.app(scope, receive, send)
File "/usr/local/lib/python3.11/site-packages/starlette/routing.py", line 718, in __call__
await route.handle(scope, receive, send)
File "/usr/local/lib/python3.11/site-packages/starlette/routing.py", line 276, in handle
await self.app(scope, receive, send)
File "/usr/local/lib/python3.11/site-packages/starlette/routing.py", line 66, in app
response = await func(request)
File "/usr/local/lib/python3.11/site-packages/fastapi/routing.py", line 291, in app
content = await serialize_response(
File "/usr/local/lib/python3.11/site-packages/fastapi/routing.py", line 154, in serialize_response
raise ResponseValidationError(
fastapi.exceptions.ResponseValidationError
@Chiggy-Playz I used code that you provided and I can't see any issue:
import enum
from datetime import datetime
from fastapi import APIRouter, Depends, Query
from pydantic import AliasPath, BaseModel, Field
from sqlalchemy import Boolean, DateTime, Enum, ForeignKey, Integer, String, TypeDecorator, UniqueConstraint, select
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, MappedAsDataclass, mapped_column, relationship
from fastapi_pagination import Page, add_pagination
from fastapi_pagination.ext.sqlalchemy import paginate
class GameType(enum.Enum):
"""Enum for game type."""
ase = "ase"
asa = "asa"
class Platform(enum.Enum):
"""Enum for platform."""
xbox = "Xbox"
playstation = "Playstation"
pc = "PC"
unknown = "Unknown"
any = "Any"
class PlatformType(TypeDecorator):
"""Platform type for server model."""
impl = JSONB
cache_ok = True
def process_bind_param(self, value, dialect):
"""Used to convert enum to string."""
if value:
value = [platform.value for platform in value]
return value
def process_result_value(self, value, dialect):
"""Used to convert string to enum."""
if value:
value = [Platform(platform) for platform in value]
return value
class Base(DeclarativeBase, MappedAsDataclass, kw_only=True):
pass
class ServerModel(Base):
"""Model for server."""
__tablename__ = "servers"
__table_args__ = (
UniqueConstraint(
"game_type",
"name",
"platforms",
"official",
name="unique_server",
),
)
id: Mapped[int] = mapped_column(Integer, autoincrement=True, primary_key=True)
game_type: Mapped[GameType] = mapped_column(Enum(GameType), nullable=False)
name: Mapped[str] = mapped_column(String, nullable=False)
platforms: Mapped[list[Platform]] = mapped_column(
PlatformType,
nullable=False,
)
official: Mapped[bool] = mapped_column(Boolean, nullable=False)
map: Mapped[str] = mapped_column(String, nullable=False)
details: Mapped["ServerDetailsModel"] = relationship(
back_populates="server",
lazy="joined",
)
class ServerDetailsModel(Base):
"""Model for server_details."""
__tablename__ = "server_details"
id: Mapped[int] = mapped_column(ForeignKey("servers.id"), primary_key=True)
players: Mapped[int] = mapped_column(Integer, nullable=False)
ip: Mapped[str] = mapped_column(String, nullable=False)
port: Mapped[int] = mapped_column(Integer, nullable=False)
day: Mapped[int] = mapped_column(Integer, nullable=False)
last_seen_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
)
download_characters: Mapped[bool] = mapped_column(Boolean, nullable=False)
download_items: Mapped[bool] = mapped_column(Boolean, nullable=False)
server: Mapped[ServerModel] = relationship(back_populates="details", default=None)
def _details_alias(name: str) -> AliasPath:
return AliasPath("details", name)
class ServerResponse(BaseModel):
"""Server Response."""
model_config = {
"populate_by_name": True,
}
id: int
game_type: GameType
name: str
platforms: list[str]
official: bool
map: str
players: int = Field(validation_alias=_details_alias("players"))
ip: str = Field(validation_alias=_details_alias("ip"))
port: int = Field(validation_alias=_details_alias("port"))
day: int = Field(validation_alias=_details_alias("day"))
download_items: bool = Field(validation_alias=_details_alias("download_items"))
download_characters: bool = Field(validation_alias=_details_alias("download_characters"))
last_seen_at: datetime = Field(validation_alias=_details_alias("last_seen_at"))
engine = create_async_engine(
"postgresql+asyncpg://postgres:postgres@localhost:5433/postgres",
future=True,
)
async def get_db_session():
async with AsyncSession(engine) as session:
yield session
router = APIRouter()
@router.get("/", response_model=Page[ServerResponse])
async def filter_servers(
query: str = Query(description="Query to filter servers"),
game_type: GameType = Query(None, description="Game type of server"),
platform: list[Platform] = Query(None, description="Platform(s) of server"),
official: bool = Query(None, description="Whether server is official"),
db: AsyncSession = Depends(get_db_session),
):
statement = select(ServerModel).where(ServerModel.name.ilike(f"%{query}%"))
if game_type is not None:
statement = statement.where(ServerModel.game_type == game_type)
if platform is not None:
statement = statement.where(ServerModel.platforms == platform)
if official is not None:
statement = statement.where(ServerModel.official == official)
return await paginate(db, statement)
async def lifespan(_):
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)
async with AsyncSession(engine) as session:
async with session.begin():
# Create some servers
session.add_all(
[
ServerModel(
id=1,
game_type=GameType.ase,
name="server1",
platforms=[Platform.xbox, Platform.playstation],
official=True,
map="map1",
details=ServerDetailsModel(
id=1,
players=10,
ip="1.1.1.1",
port=1234,
day=1,
last_seen_at=datetime.now(),
download_characters=True,
download_items=True,
),
),
],
)
yield
app = APIRouter(lifespan=lifespan)
add_pagination(app)
app.include_router(router, prefix="/servers")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app)
When I call endpoint it returns me:
{
"items": [
{
"id": 1,
"game_type": "ase",
"name": "server1",
"platforms": [
"Xbox",
"Playstation"
],
"official": true,
"map": "map1",
"players": 10,
"ip": "1.1.1.1",
"port": 1234,
"day": 1,
"download_items": true,
"download_characters": true,
"last_seen_at": "2024-02-20T18:23:23.298450Z"
}
],
"total": 1,
"page": 1,
"size": 50,
"pages": 1
}
Could you please check code, maybe you missed stmth on your side?
One thing that I noticed, that you didn't await paginate
, that might be an issue.
@router.get("/", response_model=Page[ServerResponse])
async def filter_servers(
query: str = Query(description="Query to filter servers"),
game_type: GameType = Query(None, description="Game type of server"),
platform: list[Platform] = Query(None, description="Platform(s) of server"),
official: bool = Query(None, description="Whether server is official"),
db: AsyncSession = Depends(get_db_session),
):
statement = select(ServerModel).where(
ServerModel.name.ilike(f"%{query}%"),
)
if game_type is not None:
statement = statement.where(ServerModel.game_type == game_type)
if platform is not None:
statement = statement.where(ServerModel.platforms == platform)
if official is not None:
statement = statement.where(ServerModel.official == official)
return await paginate(db, statement)
Oh....
It seems the missing await
fixed the issue 😅
I feel like an idiot now :)
Tysm!
No worries, I am happy to help you)
Also, thank you for the validation_alias=_details_alias("field")
thingy! It was exactly what i was looking for 😁
Always happy to help)