Ahmet-Salman
Ahmet-Salman

Reputation: 345

Returning a List response model with mutliple pydantic models FastAPI

Pydantic Model

class PostingType(str, Enum):
    house_seeker = 'House Seeker'
    house_sharer = 'House Sharer'

class ResponsePosting(BaseModel):
    id: UUID = Field(default_factory=uuid4)
    timestamp: date = Field(default_factory=date.today)
    title: str = Field(default=...)
    description: str = Field(default=...)
    
class ResponseHouseSeekerPosting(ResponsePosting):
    postingType: Literal[PostingType.house_seeker]

class ResponseHouseSharerPosting(ResponsePosting):
    postingType: Literal[PostingType.house_sharer]
    price: conint(ge=1)
    houseSize: HouseSize = Field(default=...)

class ResponseGetPost(BaseModel):
    __root__: Union [ResponseHouseSharerPosting, ResponseHouseSeekerPosting] = Field(default=..., discriminator='postingType')

Goal

I have an endpoint that will return a list of either ResponseHouseSeekerPosting or ResponseHouseSharerPosting, I am trying to figure out what I should write for the response model to make it work

The endpoint looks like this:

@router.get(path='/posts', response_description="Retrieves a list of postings of the user", status_code=200)
async def get_user_posts(post_type: PostingType):
    
    user = users_db_connection.aggregate("Some Aggregate Query that returns the list of posts for the user, this works properly")
    
    async for doc in user:
        if post_type == PostingType.house_seeker:
            return doc['postings'] # <-- this is type <list> and holds a list of *ResponseHouseSeekerPosting*
        else:
            return doc['postings'] # <-- this is type <list> and holds a list of *ResponseHouseSharerPosting*

Issue

So I have tried many things to get this to work but I can't figure it out, I have checked the following links but to no avail:

All of the above answer my question but only for a single returned element (by using a Discriminated Unions), I have an endpoint that uses ResponseGetPost to return either models and it works fine, the issue arises when I try to use it for a list

Attempts

Attempt 1: Adding a new model

I tried to add a new model like this

class ResponseGetPostList(BaseModel):
    list_of_posts : List[ResponseGetPost]

and tried this too

class ResponseGetPostList(BaseModel):
    __root__: List[ResponseGetPost]

with the endpoint looking like this:

@router.get(path='/posts', response_description="Retrieves a list of postings of the user", status_code=200, response_model=ResponseGetPostList) <-- the response_model changed
async def get_user_posts(post_type: PostingType):
    
    user = users_db_connection.aggregate("Some Aggregate Query that returns the list of posts for the user, this works properly")
    
    async for doc in user:
        if post_type == PostingType.house_seeker:
            return doc['postings'] # <-- this is type <list> and holds a list of *ResponseHouseSeekerPosting*
        else:
            return doc['postings'] # <-- this is type <list> and holds a list of *ResponseHouseSharerPosting*

P.S I was sending a body of a house seeker post But it did not work, and this is the error message that appeared:

  File "D:\BilMate\BilMate-Backend\venv\Lib\site-packages\fastapi\routing.py", line 145, in serialize_response
    raise ValidationError(errors, field.type_)
pydantic.error_wrappers.ValidationError: 3 validation errors for ResponseGetPost
response -> 0 -> __root__ -> postingType
  unexpected value; permitted: <PostingType.house_sharer: 'House Sharer'> (type=value_error.const; given=House Seeker; permitted=(<PostingType.house_sharer: 'House Sharer'>,)) <-- this always becomes the opposite of what I provide in the ```post_type``` variable
response -> 0 -> __root__ -> price
  field required (type=value_error.missing)
response -> 0 -> __root__ -> houseSize
  field required (type=value_error.missing)

Attempt 2

Similar to Attempt 1 but without a new model I simply added it like this

@router.get(path='/posts', response_description="Retrieves a list of postings of the user", status_code=200, response_model=List[ResponseGetPost])

I got the same error as Attempt 1

Attempt 3

Tried this endpoint response_model but changed the return value:

@router.get(path='/posts', response_description="Retrieves a list of postings of the user", status_code=200, response_model=List[ResponseGetPost])
async def get_user_posts(post_type: PostingType):
    
    user = users_db_connection.aggregate("Some Aggregate Query that returns the list of posts for the user, this works properly")
    
    async for doc in user:
        if post_type == PostingType.house_seeker:
            print(doc['postings'])
            return ResponseHouseSeekerPosting(doc['postings']) <-- This Changed
        else:
            return ResponseHouseSharerPosting(doc['postings']) <-- This Changed

Also, once again, it did not work. This is the error message I was met with:

File "pydantic\main.py", line 332, in pydantic.main.BaseModel.__init__
TypeError: __init__() takes exactly 1 positional argument (2 given)

Notes

I have an endpoint @router.get('/{post_id}', response_description='retrieves a single post', response_model=ResponseGetPost) that does the same thing I am trying to achieve here but this endpoint only returns a single element. This endpoint works fine so I tried to replicate it but for a list but I'm stuck and would appreciate any help

Upvotes: 1

Views: 1124

Answers (1)

Baktybek Baiserkeev
Baktybek Baiserkeev

Reputation: 675

you can specify response_model in the route. I guess it can help.

from typing import Union, List

@app.get(
    path='/posts', 
    response_description="Retrieves a list of postings of the user", 
    status_code=200, 
    response_model=Union[List[ResponseHouseSharerPosting], List[ResponseHouseSeekerPosting]]
)
async def get_user_posts(post_type: PostingType):
    #  rest of the code ...
    async for doc in user:
        if post_type == PostingType.house_seeker:
            return [ResponseHouseSeekerPosting(**posting) for posting in doc['postings']] 
        else:
            return [ResponseHouseSharerPosting(**posting) for posting in doc['postings']]

Upvotes: 2

Related Questions