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}