Fastapi: [QUESTION] Am I doing something wrong with my models? Getting a validation error on my response.

Created on 31 Jul 2019  路  9Comments  路  Source: tiangolo/fastapi

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)

question

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 = True on the config it would help? I'm not sure.)

All 9 comments

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

@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:

Was this page helpful?
0 / 5 - 0 ratings