Description
I am getting a validation error on the response for my API calls that retrieve users, and I am not quite sure where to g in terms fixing it.
I am using pieces of the postgres full stack example.
Once I log into the api docs, I am able to make the request to retrieve user information, but the request does not complete. A validation exception is raised by pydantic for the response object.
I have included the error details below and I setup a repo that recreates what I am experiencing. I think I am just missing something simple. Thanks in advance for any assistance!
Example Repo: https://github.com/Rehket/API-Issue
Traceback
Traceback (most recent call last):
File "C:\Users\adama\.virtualenvs\TryFast\lib\site-packages\uvicorn\protocols\http\h11_impl.py", line 375, in run_asgi
result = await app(self.scope, self.receive, self.send)
File "C:\Users\adama\.virtualenvs\TryFast\lib\site-packages\starlette\applications.py", line 133, in __call__
await self.error_middleware(scope, receive, send)
File "C:\Users\adama\.virtualenvs\TryFast\lib\site-packages\starlette\middleware\errors.py", line 177, in __call__
raise exc from None
File "C:\Users\adama\.virtualenvs\TryFast\lib\site-packages\starlette\middleware\errors.py", line 155, in __call__
await self.app(scope, receive, _send)
File "C:\Users\adama\.virtualenvs\TryFast\lib\site-packages\starlette\middleware\base.py", line 25, in __call__
response = await self.dispatch_func(request, self.call_next)
File "C:/Users/adama/Workspace/Python/TryFast/app/main.py", line 34, in db_session_middleware
response = await call_next(request)
File "C:\Users\adama\.virtualenvs\TryFast\lib\site-packages\starlette\middleware\base.py", line 45, in call_next
task.result()
File "C:\Users\adama\.virtualenvs\TryFast\lib\site-packages\starlette\middleware\base.py", line 38, in coro
await self.app(scope, receive, send)
File "C:\Users\adama\.virtualenvs\TryFast\lib\site-packages\starlette\middleware\cors.py", line 76, in __call__
await self.app(scope, receive, send)
File "C:\Users\adama\.virtualenvs\TryFast\lib\site-packages\starlette\exceptions.py", line 73, in __call__
raise exc from None
File "C:\Users\adama\.virtualenvs\TryFast\lib\site-packages\starlette\exceptions.py", line 62, in __call__
await self.app(scope, receive, sender)
File "C:\Users\adama\.virtualenvs\TryFast\lib\site-packages\starlette\routing.py", line 585, in __call__
await route(scope, receive, send)
File "C:\Users\adama\.virtualenvs\TryFast\lib\site-packages\starlette\routing.py", line 207, in __call__
await self.app(scope, receive, send)
File "C:\Users\adama\.virtualenvs\TryFast\lib\site-packages\starlette\routing.py", line 40, in app
response = await func(request)
File "C:\Users\adama\.virtualenvs\TryFast\lib\site-packages\fastapi\routing.py", line 122, in app
skip_defaults=response_model_skip_defaults,
File "C:\Users\adama\.virtualenvs\TryFast\lib\site-packages\fastapi\routing.py", line 54, in serialize_response
raise ValidationError(errors)
pydantic.error_wrappers.ValidationError: 1 validation error
response
value is not a valid dict (type=type_error.dict)
I can see the values of the object being returned are correct, bust it seems like pydantic is having trouble massaging the response into something it can send.

@Rehket what versions of pydantic and fastapi are you using? Edit: nevermind, I saw the versions in your req.txt and they seem good.
@dmontagu Thanks for taking a look! I think I'm probably going to go without the ORM for a bit until I'm sure I have grokked pydantic and sqlalchemy.
It looks like a problem converting the sqlalchemy model to a pydantic model. Could you just test manually converting it to the pydantic model first? So at the end, rather than returning user (which is actually of type DBUser in your code), create a new (pydantic) User from user and return that.
Whether it works or not, it will probably shed some light on the issue; we can try to figure out how to get it working as expected after that.
@dmontagu Converting to the Pydantic model or a Dict before returning the response work. Thanks for the suggestion!
@Rehket I don't think you should need to do that, but I'm not sure what is causing the issue. It might be a model config issue? (Maybe if you set orm_mode = True on the config it would help? I'm not sure.)
Setting orm_mode=True in the base model worked as well and looks much better, thank you for the suggestion!
@Rehket for what it's worth, if you end up wanting to use orm_mode=True on most of your models, I recommend subclassing BaseModel and using the subclass as a base class for all of your API models (to reduce code repetition). For example, I use a base class that mostly looks like this:
import re
from functools import partial
from typing import Any, Dict
from fastapi.encoders import jsonable_encoder
from pydantic import BaseConfig, BaseModel
def snake2camel(snake: str, start_lower: bool = False) -> str:
camel = snake.title()
camel = re.sub("([0-9A-Za-z])_(?=[0-9A-Z])", lambda m: m.group(1), camel)
if start_lower:
camel = re.sub("(^_*[A-Z])", lambda m: m.group(1).lower(), camel)
return camel
class APIModel(BaseModel):
class Config(BaseConfig):
orm_mode = True
allow_population_by_alias = True
alias_generator = partial(snake2camel, start_lower=True)
def dump_obj(self, **jsonable_encoder_kwargs: Any) -> Dict[str, Any]:
return jsonable_encoder(self, **jsonable_encoder_kwargs)
This gives me automatic generation of camelcase aliases, and the dump_obj method (which generates a more json-dumps-friendly dict than model.dict(), but is still actually a dict).
For example:
import uuid
class User(APIModel):
user_id: uuid.UUID
email: str
user = User(user_id=uuid.uuid4(), email="[email protected]")
print(repr(user.json(by_alias=True)))
# '{"userId": "7976b33a-ffcb-43c2-8716-a5b1d492e8da", "email": "[email protected]"}'
print(repr(user.dict(by_alias=True)))
# {'userId': UUID('7976b33a-ffcb-43c2-8716-a5b1d492e8da'), 'email': '[email protected]'}
# Notice neither .json() or .dict() give exactly the following:
print(repr(user.dump_obj()))
# {'userId': '7976b33a-ffcb-43c2-8716-a5b1d492e8da', 'email': '[email protected]'}
The aliases also play nice with FastAPI and result in the generated schema using camelcase, which seems to be a more common convention outside of python.
Thanks for all the help here @dmontagu ! :cake: :taco:
And thanks @Rehket for reporting back and closing the issue. :heavy_check_mark:
Most helpful comment
@Rehket I don't think you should need to do that, but I'm not sure what is causing the issue. It might be a model config issue? (Maybe if you set
orm_mode = Trueon the config it would help? I'm not sure.)