如何在FastAPI+MongoDB环境下用Pydantic更新嵌套模型
MongoDB数组元素更新异常问题
问题场景
我尝试更新用户文档中指定名称的角色信息,执行后却在characters数组里新增了一个独立元素,而非修改目标角色的字段。
我的Pydantic模型
class User(BaseModel): id: int = Field(default_factory=uuid.UUID, alias="_id") post_count: int last_awarded: int points: int characters: list[Character] class CharacterUpdate(BaseModel): name: Optional[str] = None type: Optional[str] = None xp: Optional[int] = None
FastAPI路由代码
@router.put("/{id}/{name}", response_description="Update a character", response_model=Character) def update_character(id: int, name:str, request: Request, char: CharacterUpdate = Body(...)): char = {k: v for k, v in char.model_dump().items() if v is not None} if len(char) >= 1: update_result = request.app.database["users"].update_one( {"_id": id, "characters.name": name}, {"$set": char} )
原始MongoDB用户文档
{ "_id": 12345, "post_count": 0, "last_awarded": 0, "points": 0, "characters": [ { "name": "Person 1", "type": "Character", "xp": 900 }, { "name": "Person 2", "type": "NPC", "xp": 900 } ] }
测试用curl请求
curl -X 'PUT' \ 'http://localhost:8000/user/12345/Person%201' \ -H 'accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ "name": "Person 3" }'
预期与实际结果
- 预期:将
Person 1的name字段改为Person 3,保留原有type和xp字段 - 实际:
characters数组新增了独立元素"name": "Person 3",原角色未修改:
{ "_id": 12345, "post_count": 0, "last_awarded": 0, "points": 0, "characters": [ { "name": "Person 1", "type": "Character", "xp": 900 }, { "name": "Person 2", "type": "NPC", "xp": 900 }, "name": "Person 3" ] }
需求:路由需支持动态更新——比如请求修改type和xp时,保留name不变,以此类推。
解决方案
问题根源是MongoDB的$set操作符使用错误:直接传入字段键值对时,MongoDB无法识别要更新数组中的嵌套元素,会把内容直接添加到数组或文档根级别。需要用**位置操作符$**定位匹配到的数组元素。
修改后的路由代码:
from fastapi import HTTPException @router.put("/{id}/{name}", response_description="Update a character", response_model=Character) def update_character(id: int, name:str, request: Request, char: CharacterUpdate = Body(...)): # 过滤空值,只保留需要更新的字段 update_data = {k: v for k, v in char.model_dump().items() if v is not None} if not update_data: raise HTTPException(status_code=400, detail="没有需要更新的字段") # 构造$set的目标字段,用characters.$.字段名定位数组中的匹配元素 set_fields = {f"characters.$.{key}": value for key, value in update_data.items()} update_result = request.app.database["users"].update_one( {"_id": id, "characters.name": name}, {"$set": set_fields} ) # 检查是否匹配到目标文档 if update_result.matched_count == 0: raise HTTPException(status_code=404, detail="用户或角色不存在") # 查询并返回更新后的角色信息 updated_user = request.app.database["users"].find_one({"_id": id}) updated_char = next(c for c in updated_user["characters"] if c["name"] == update_data.get("name", name)) return updated_char
关键说明
- 位置操作符
$:MongoDB会自动将$替换为匹配到的数组元素索引,确保$set精准修改目标元素的指定字段,而非新增元素。 - 动态构造更新字段:把请求中的每个字段转换为
characters.$.字段名格式,保证所有更新都作用于数组内的目标角色。 - 结果校验:增加匹配计数检查,避免无提示失败;最后查询返回更新后的角色,符合响应模型要求。
修改后执行原curl请求,即可正确将Person 1的name改为Person 3,且保留原有type和xp。若请求修改type或xp,也只会更新对应字段,不影响其他内容。
内容的提问来源于stack exchange,提问作者SinicalDreams
相关产品推荐
相关产品推荐

