PyMongo查询结果含ObjectId等BSON类型,原生json.dumps()不支持,需用bson.json_util.dumps()序列化为MongoDB Extended JSON格式,保留类型信息且可逆。

PyMongo 查询结果里的 ObjectId 不是字符串,而是 BSON 类型对象,Python 原生 json.dumps() 根本不认识它 —— 所以报错 TypeError: Object of type ObjectId is not JSON serializable,不是你代码写错了,是工具用错了。
为什么 json.dumps() 会直接崩溃
Python 的 json 模块只认基础类型:dict、list、str、int、float、bool、None。而 PyMongo 返回的文档里,_id 字段是 bson.ObjectId 实例,created_at 可能是带时区的 datetime,还有 Decimal128、DBRef 等——全都不在 json.JSONEncoder 的默认支持列表里。
常见触发场景:
- Flask 中直接
return jsonify(user),其中user是find_one()结果 - Django REST Framework 序列化器没处理
_id,直接传给JSONRenderer - 日志里写
logger.info(json.dumps(doc)),一执行就崩
bson.json_util.dumps() 是最省心的方案
它专为 MongoDB 场景设计,能把 ObjectId、datetime 等转成 MongoDB Extended JSON 格式,比如:
{"_id": {"$oid": "60a8b1c2d3e4f5a6b7c8d9e0"}, "created_at": {"$date": {"$numberLong": "1747045800000"}}}
这种格式前端或 Node.js 侧用 EJSON.parse() 可无损还原,不丢精度、不丢类型语义。
但注意两点:
- 返回的是字符串,不是 Python dict,别再套一层
json.loads() - 它默认输出带类型标记的 JSON(如
$oid),如果前端只想要干净字符串 ID,这个方案反而多了一步解析
想直接转成“干净 JSON”?用自定义 JSONEncoder
如果你的 API 前端只接受标准 JSON(比如 _id: "60a8b1c2d3e4f5a6b7c8d9e0"),那就得自己控制转换逻辑:
推荐写法:
import json
from bson import ObjectId
class MongoJSONEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, ObjectId):
return str(obj)
if hasattr(obj, 'isoformat'):
return obj.isoformat()
return super().default(obj)
json.dumps(data, cls=MongoJSONEncoder)
关键点:
- 必须显式检查
isinstance(obj, ObjectId),不能只靠str(obj)—— 否则遇到None或非法字符串会静默出错 -
hasattr(obj, 'isoformat')比isinstance(obj, datetime)更安全,兼容date、datetime、甚至某些第三方时间类 - 千万别在生产环境裸写
user['_id'] = str(user['_id']),游标遍历、嵌套文档、数组里含_id都会漏掉
从请求参数解析 ObjectId 时最容易翻车
前端传来的 _id 总是字符串,但直接喂给 ObjectId() 极易触发 bson.errors.InvalidId 异常:
典型错误信息:"abc" is not a valid ObjectId, it must be a 24-character hex string
原因包括:
- 用户输入了空值、
None、带空格的字符串(如" 507f1f77bcf86cd799439011 ") - 传了 MongoDB Shell 里显示的格式:
ObjectId("507f1f77bcf86cd799439011") - 长度不对:23 位或 25 位十六进制字符
安全做法永远是先校验再构造:
from bson import ObjectId
raw_id = request.args.get("id", "")
if not ObjectId.is_valid(raw_id):
abort(400, "Invalid _id format")
doc = collection.find_one({"_id": ObjectId(raw_id)})
ObjectId.is_valid() 是 PyMongo 4.0+ 提供的轻量校验,比 try/except 更快,也避免把异常当流程控制。
真正麻烦的不是序列化本身,而是混用场景:同一份数据既要给前端 JSON、又要存回 MongoDB、还要写进日志。这时候硬塞 str() 或盲目用 json_util 都会埋坑。关键是根据下游消费方决定输出形态,而不是统一“转成字符串”了事。


















