Beanie 的 update_one() 默认绕过 Pydantic 字段验证,仅做基础 schema 匹配,不执行 @field_validator 或 model_config 约束,导致 min_length 等校验失效;需改用 doc.save() 或手动前置 model_validate()。

Beanie update_one 绕过 Pydantic 验证导致写入非法值
Beanie 的 update_one()、find_one_and_update() 等底层调用 pymongo 原生命令,**默认不触发 Pydantic 的 @field_validator 或 model_config 约束**。它只做基础 schema 匹配(比如字段是否存在、BSON 类型是否大致符合),但不会跑模型级校验逻辑。
常见错误现象:name 字段设了 min_length=3,却用 update_one({"$set": {"name": "a"}}) 成功写入 —— 这不是 Beanie 漏了,是设计如此。
- 想走完整验证:改用
doc.save()(先查出 Document 实例,改字段,再 save) - 想保持原子更新但又需校验:手动前置调用
MyModel.model_validate({"name": "a"}),捕获ValidationError - 高频更新场景注意:用
save()会多一次 round-trip(先 find 再 replace),延迟比原生 update 高
Beanie 的 Document 实例 vs as_pydantic() 导致 save 失效
Beanie 的 find() 默认返回的是 Document 子类实例,自带变更跟踪和数据库方法(如 .save())。一旦你调用 .as_pydantic() 或转成 dict 再构造新模型,就丢失了这些能力 —— 此时调用 .save() 不会触发增量更新,甚至可能静默失败。
常见错误现象:执行 doc.name = "new" 后 doc.save() 没反应,数据库字段没变。
- 确认你用的是原始
Document实例,不是.as_pydantic()返回的BaseModel - 避免中间转
dict或model_dump()后再 new 模型;需要纯数据时,只在最后一步调用.model_dump() - 如果必须转成 Pydantic 模型,记得用
MyModel(**doc.model_dump()),但此时.save()已无效,只能走 insert/replace
MongoDB 层面的 $jsonSchema 校验与 BSON 类型不匹配
即使 Beanie 模型验证通过,MongoDB 服务端也可能拒绝写入,报错 "Document failed validation"。根本原因常是 BSON 类型不一致:比如你在 $jsonSchema 里写 bsonType: "int",但 PyMongo 插入的 {"age": 25} 实际存为 double,校验失败。
常见解决方式:
- PyMongo 中显式用
Int32(25)或Int64(25)替代普通整数 - 把 schema 改成
bsonType: ["int", "long", "double"],或更稳妥地用bsonType: "number"(MongoDB 5.0+ 支持) - 检查嵌套字段:MongoDB 的
required只作用于顶层字段,config对象里缺timeout不会报错,除非你在config.properties里也声明required - 调试时加
try/except WriteError as e: print(e.details)—— 注意 PyMongo ≥ 4.2 才暴露e.details
MotorClient 初始化不当导致 Beanie 协程卡死
Beanie 依赖 Motor,而 MotorClient 若未在 async 上下文中创建,所有查询会阻塞或静默失败 —— 尤其在 FastAPI 依赖注入里直接 MotorClient() 最危险。
常见错误现象:await collection.find({}).to_list() 永远不返回,日志无 MongoDB 查询记录。
- 推荐初始化方式:
AsyncIOMotorClient("mongodb://...")(Motor 本身已适配 asyncio) - 别混用:确保
AsyncIOEngine和AsyncIOMotorClient配对,SyncIOEngine已在 odmantic/beanie 0.15+ 弃用 - FastAPI 中用
Depends注入时,务必用async def get_db()+await client.get_database(),而非同步获取
save(),实际操作的是一个已脱离状态跟踪的副本;或者你以为 schema 写对了,但 BSON 类型根本对不上。


















