Home > Back-end >  FastAPI - How to set correct response_model to return n documents from MongoDB?
FastAPI - How to set correct response_model to return n documents from MongoDB?

Time:08-19

Working on a FastAPI project using a MongoDB backend (using motor-asyncio). I will caveat all of this by saying that I'm very new to both FastAPI and motor (previously just worked with PyMongo).

I'm working on an endpoint that is supposed to return n documents from Mongo, contingent on the params set. I am able to get this to work if I don't set a response model in the function definition, and simply do the following:

@ScoresRouter.get("/getScores")
# omitted further query params for simplicity. there are a few of them which then alter the query
# but this is the simplest version
async def get_scores(members: list[str] | None = Query(default=None)):
    c = coll.find({'membershipNumber' : {'$in' : members}}, {'_id' : 0, 'membershipNumber' : 1, 'score' : 1, 'workplace' : 1, 'created_at' : 1} 
    out = []
    async for doc in c:
        out.append(doc)

    return out

But, I want to use the proper pydantic response_model syntax. So, I've defined the following:

class ScoreOut(BaseModel):
    membershipNumber : str 
    score : float | None 
    historic_data : list | None
    created_at : datetime | None
    region : str | None
    district : str | None
    branch : str | None
    workplace : str | None

class ScoresOut(BaseModel):
    result : List[ScoreOut]

This is in accordance with what the data looks like in my target DB collection, which is this (this is copied from the mongo shell, not python):

mydb:PRIMARY> db.my_coll.findOne();
{
    "_id" : ObjectId("1234"),
    "membershipNumber" : "M123456"
    "score" : 8.3,
    "workplace" : "W0943294",
    "created_at" : ISODate("2022-07-09T23:00:04.070Z"),
    "end_date" : ISODate("2022-07-09T00:00:00Z"),
    "historical_data" : [
        {
            "score" : 0,
            "created_at" : ISODate("2022-05-10T16:50:19.136Z"),
            "end_date" : ISODate("2020-01-08T00:00:00Z")
        },
        {
            "score" : 0,
            "end_date" : ISODate("2020-01-15T00:00:00Z"),
            "created_at" : ISODate("2022-05-10T16:55:21.644Z")
        }
    ]
}

Now, I change the route/function definition as follows:

async def get_scores(members: list[str] | None = Query(default=None),
                     response_model=ScoresOut,
                     response_model_exclude_unset=True):
    c = coll.find({'membershipNumber' : {'$in' : members}}, {'_id' : 0, 'membershipNumber' : 1, 'score' : 1, 'workplace' : 1, 'created_at' : 1} 
    out = []
    async for doc in c:
        out.append(doc)

    return out

And it no longer works. On the swagger-GUI I get a rather uninformative Internal Server Error, but in my terminal I'm getting this error:

pydantic.error_wrappers.ValidationError: 1 validation error for ScoresOut
response
  value is not a valid dict (type=type_error.dict)

I imagine I somehow have to tell my function to wrap out in the ScoresOut response model, although a lot of tutorials I've seen do not do this step: They simple output an object which appears to match the response_model they've defined, and it somehow just works.

I wonder if this has something to do with Mongo's rather difficult bson datatypes, and converting them to something FastAPI/pydantic understand? I doubt it though, because if I don't define a response-model and simply return the out object, it works, and it looks as it would if I'd print the list of dicts in Python.

Any help with this would be hugely appreciated.

CodePudding user response:

Since you're returning a list and not a single object, you'll have to tell FastAPI that it can expect a list of objects compatible with ScoresOut to be returned:

@ScoresRouter.get("/getScores", response_model=List[ScoresOut])

(side note: generally I recommend using just /scores as the path, as GET is implied by the HTTP method you're using)

If your mongodb library returns objects (instead of dictionaries), you'll need to configure your model to load its values from property lookups as well (.foo):

class ScoresOut(BaseModel):
    result : List[ScoreOut]

    class Config:
        orm_mode = True

And as long as your mongodb library returns a iterable, you don't have to create the list and iterate it yourself:

async def get_scores(members: list[str] | None = Query(default=None),
                     response_model=List[ScoresOut],
                     response_model_exclude_unset=True):
    return coll.find({'membershipNumber' : {'$in' : members}}, {'_id' : 0, 'membershipNumber' : 1, 'score' : 1, 'workplace' : 1, 'created_at' : 1} 
  • Related