1. 先看这条报错:Object of type ObjectId is not JSON serializable
用 PyMongo 写数据接口时,最容易在半路拦人的就是这条:TypeError: Object of type ObjectId is not JSON serializable。它的触发点很固定:collection.find()返回的是 Cursor 游标,迭代出来的每一条 doc 里带着_id这个 ObjectId,也可能带着datetime类型的manufacture_date。Python 内置json模块只认字典、列表、字符串、数字、布尔和 None,遇到 ObjectId 直接丢出 TypeError,整段程序停在那里,日志里只留下这一行让人摸不着头脑。
把 Cursor 转成 JSON 本身不是难点,难的是让 BSON 特殊类型既能被 JSON 序列化,又不在转换中丢失类型信息。原教程里给过一条清晰路线:手动转换、pymongo.json_util、流式输出,三种办法各有适用场景。这篇不说空话,直接用我们手头的报错走一遍,重点落在官方推荐的第 5 节json_util方案上。同时把 AI 编程工具 Codex 也拉进这条链路:先在 TaoToken 拿一把 API Key,把 Codex 的模型通道 Base URL 填成https://taotoken.net/api,然后让 Codex 按json_util的思路改写转换代码,既保留 ObjectId 信息,又不再被内置json模块卡住。
2. 还原现场:Cursor 里到底有什么
2.1 一条典型的 PyMongo 查询
先看最常见的代码写法:
import json from pymongo import MongoClient from bson.objectid import ObjectId from datetime import datetime client = MongoClient("mongodb://localhost:27017/") db = client["demo_db"] collection = db["demo_products"] cursor = collection.find({}) documents = list(cursor) # 这行会报 TypeError: Object of type ObjectId is not JSON serializable result = json.dumps(documents, ensure_ascii=False) print(result)这里的cursor是 PyMongo Cursor,迭代出的documents中的每条文档都带着_id字段,类型是bson.objectid.ObjectId。如果文档里有datetime.datetime字段,比如manufacture_date,那么你还会多看到一条TypeError: Object of type datetime is not JSON serializable。两条报错本质相同:内置json模块的默认序列化规则处理不了这些 Python 对象。
2.2 为什么不能直接把 Cursor 丢给 json.dumps
Cursor 是懒加载的迭代器,它从 MongoDB 服务器分批拉取文档,而不是一次性把结果全部载入内存。json.dumps()需要的是一个实实在在的 Python 对象,你要么list()它,要么遍历它。即便list()之后,doc 里的 ObjectId 和 datetime 依然是 BSON 类型,json模块不认。原教程里说得很实在:内置json模块只适合文档里全是常规类型的简单场景,一旦出现 BSON 特殊类型,必须自己接管转换逻辑。
2.3 官方推荐路径在原文里的位置
原教程第 5 节给出了标准答案:pymongo.json_util。它专门处理 MongoDB Extended JSON,dumps()和dump()会自动把 ObjectId 转成{"$oid": "..."}或字符串形式,把 datetime 转成 ISO 8601 字符串或{"$date": ...}。我们接下来就用这条路改写出可运行的代码,但先做一件前提准备:给 Codex 配上能用的模型通道。
3. 准备材料:TaoToken 拿 Key,Codex 换通道
3.1 去官网创建 API Key
要让人工智能工具替我们改写转换代码,第一步是给工具一个能连上的模型接口。打开 TaoToken,注册登录后进入控制台,在 API Keys 页面点创建,把生成的 Key 复制保存。这一步对应原文里的「环境准备与 MongoDB 连接」——原文要准备的是 PyMongo 和 MongoDB 服务,我们要额外准备的是一把可用的 API Key,以及给 Codex 换一个能稳定调用的通道。
创建好的 Key 是一串类似YOUR_API_KEY的占位字符串,后面所有配置里都用这个占位符指代。注意,这串 Key 只出现在你的本地配置文件中,不要提交到 Git 仓库,也不要粘到前端页面。控制台的调用记录页面会显示每一笔请求的模型、时间和 token 消耗,后续验证是否接通全靠它。
3.2 Codex 配置文件指向 TaoToken
Codex 的全局配置文件在用户目录下的~/.codex/config.toml。要让 Codex 走 TaoToken 通道,关键是设置model_provider和base_url。很多同学会把 Anthropic 的环境变量ANTHROPIC_BASE_URL套到 Codex 上,这是没用的——Codex 读的是config.toml。正确写法如下:
[model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY" wire_api = "chat"然后在同一个文件的[model]段引用这个 provider:
[model] provider = "taotoken" model = "以 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 模型广场为准"设置完毕,在终端里导出环境变量:
export TAOTOKEN_API_KEY="YOUR_API_KEY"然后启动 Codex,输入任意测试问题,比如「请用 pymongo.json_util 写一段将 Cursor 转 JSON 的示例代码」。如果配置正确,Codex 会立刻返回代码片段,不会再提示模型连接失败。Base URL 的细节这里单独说明:填进工具的地址是https://taotoken.net/api,末尾不要加/v1,更不要把官网地址填进去;官网那个https://taotoken.net/?utm_source=taotoken_aicg_blog_end只用来注册、建 Key、看模型广场和用量。
3.3 让 Codex 站在 json_util 的肩膀上
配置完成后,Codex 就是一个随时可用的 Python 编码助手。跟它对话时可以直接贴报错:TypeError: Object of type ObjectId is not JSON serializable,让它用pymongo.json_util改写代码。接下来两节的代码,我都按这种协作方式组织:先说需求,再给出 Codex 生成的方案,最后落到本地可执行的文件。
4. 方法一:内置 json 模块为什么在这里栽跟头
原文第 4 节详细讲过内置json模块的三种自救办法:手动把 ObjectId 转字符串、定义自定义 JSONEncoder、写一个default函数。这些办法不是不能用,但它要求你精确知道文档里所有 BSON 类型的分布。嵌套字典里的 ObjectId、列表里的 datetime、甚至Decimal128字段,每一处都要手动处理,漏掉一个就继续报错。
class CustomMongoEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, ObjectId): return str(obj) if isinstance(obj, datetime): return obj.isoformat() return super().default(obj)这个自定义编码器能解决顶层 ObjectId 和 datetime 的问题,但嵌套结构仍需递归处理。原教程给过prepare_doc_for_json这种递归预处理函数,写起来不短,而且每增加一种 BSON 类型就得再补一段逻辑。这正是我把 Codex 拉进来的原因:让它用官方标准方案json_util一次性收拾干净,而不是跟json模块的死角死磕。
5. 方法二:用 json_util 彻底解决 ObjectId 序列化问题
5.1 json_util.dumps 的两种输出模式
pymongo.json_util.dumps()和内置json.dumps()用法几乎一样,区别是它能自动识别 BSON 类型。默认是 RELAXED 模式,ObjectId 直接变成普通字符串,datetime 变成 ISO 8601 字符串,输出简洁易读;如果需要完整保留类型信息,改用 CANONICAL 模式,ObjectId 变成{"$oid": "..."},datetime 变成{"$date": {"$numberLong": "..."}}。
5.2 让 Codex 生成的 json_util 转换代码
在 Codex 会话里输入这条指令:
用 pymongo.json_util 写一段代码: 1. 连接 MongoDB 的 demo_db.demo_products 集合 2. collection.find({}) 得到游标 3. 用 json_util.dumps 把 docs 转成 RELAXED 模式的 JSON 字符串 4. 支持指定输出文件路径,分别用 dumps 和 dump 演示Codex 大概率会返回类似下面的代码,这也是我们期望的可执行版本:
import json from pymongo import MongoClient from pymongo.json_util import dumps, dump from bson.objectid import ObjectId from datetime import datetime client = MongoClient("mongodb://localhost:27017/") db = client["demo_db"] collection = db["demo_products"] sample_docs = [ { "_id": ObjectId(), "name": "Super Widget 1.0", "price": 29.99, "manufacture_date": datetime(2023, 1, 15, 10, 30, 0), }, { "_id": ObjectId(), "name": "Mega Gizmo X", "price": 199.0, "manufacture_date": datetime(2022, 11, 1, 14, 0, 0), }, ] collection.insert_many(sample_docs) cursor = collection.find({}) documents = list(cursor) json_string = dumps(documents, indent=2, ensure_ascii=False) print(json_string) with open("products_relaxed.json", "w", encoding="utf-8") as f: dump(documents, f, indent=2, ensure_ascii=False)运行这段代码,不会再有Object of type ObjectId is not JSON serializable,输出文件里_id显示为普通字符串。如果换成 CANONICAL 模式:
from pymongo.json_util import JSONOptions, JSONMode canonical_options = JSONOptions(json_mode=JSONMode.CANONICAL) json_string_canonical = dumps( documents, json_options=canonical_options, indent=2, ensure_ascii=False ) print(json_string_canonical)得到的是带$oid结构的 Extended JSON,适合备份后重新导入 MongoDB。两种模式的取舍,原文在第 8.4 节已经说透:对外 API 用 RELAXED,数据迁移用 CANONICAL。
5.3 如果手头有旧代码,怎么让 Codex 帮你改
把下面这段报错堆栈直接贴给 Codex:
TypeError: Object of type ObjectId is not JSON serializable再加一句说明:
把这段 json.dumps 的调用改成 pymongo.json_util.dumps, 保留 indent=2 和 ensure_ascii=False, 不要用内置 json 模块。Codex 会识别出问题出在序列化层,给出替换方案。生成代码后仍回到本地执行,把执行结果贴回对话,继续让它调整。注意,Codex 只是帮你写代码和解释报错,它不会主动连到你的 MongoDB 实例上跑查询,实际运行还是在你自己的终端里完成。
6. 方法三:大结果集时别把 list(cursor) 直接塞进内存
6.1 JSON Lines 流式写入
如果find()查出来几十万条文档,list(cursor)会把所有数据一次性加载进内存,内存占用立刻飙高。原文第 6 节给出的解法是流式转换,其中 JSON Lines 格式最简单:每行一个 JSON 对象,边遍历边写,不积压。Codex 生成的示例:
from pymongo import MongoClient from pymongo.json_util import dumps, JSONOptions, JSONMode client = MongoClient("mongodb://localhost:27017/") collection = client["demo_db"]["demo_products"] cursor = collection.find({}) output_file = "products_stream.jsonl" with open(output_file, "w", encoding="utf-8") as f: for doc in cursor: json_line = dumps( doc, json_options=JSONOptions(json_mode=JSONMode.RELAXED) ) f.write(json_line + "\n") client.close()这个脚本不管查出来多少条文档,内存里始终只有当前正在处理的一条。配合 TaoToken 控制台里的调用记录,你还能看到 Codex 帮你写这段代码时消耗了多少 token——这种调试细节在生产环境里很有用。
6.2 手动构建 JSON 数组
如果下游系统要求输出必须是一个完整的 JSON 数组,不能是 JSON Lines,那就手动控制[、,、]的写入位置。这个方案比 JSON Lines 繁琐,但可以避免一次性构建大列表。Codex 会这样写:
with open("products_array.json", "w", encoding="utf-8") as f: f.write("[\n") for index, doc in enumerate(cursor): doc_json = dumps( doc, indent=2, json_options=JSONOptions(json_mode=JSONMode.RELAXED) ) indented = "\n".join(" " + line for line in doc_json.splitlines()) f.write(indented) f.write(",\n" if index < total - 1 else "\n") f.write("]\n")这里有个容易忽略的坑:enumerate(cursor)拿到的index只能用来判断是不是最后一条文档,不能用来做任何业务编号。游标分批拉取数据时,index是连续的,但如果中间某条文档被跳过或查询结果排序变化,编号就没有实际意义。需要稳定编号时,在 MongoDB 查询阶段就用sort固定顺序,或者在循环里自己维护计数器。
7. 排障与验证:报错消掉之后,这一步别省
7.1 最常见的三个残留问题
json_util能解决原生不支持的 BSON 类型,但配完 Codex 后你可能还会撞上别的问题。第一个是AttributeError: module 'pymongo.json_util' has no attribute 'dumps',这通常是因为 PyMongo 版本太旧,升级到 4.x 即可。第二个是运行时报ServerSelectionTimeoutError,那就是 MongoDB 服务没启动或连接串写错,跟序列化无关。第三个是你把https://taotoken.net/api末尾加了/v1,导致 Codex 报 404;正确的 Base URL 就是https://taotoken.net/api,不带/v1。官网落地页和 API 地址是两回事:注册、建 Key、看模型用 TaoToken,填进工具的地址固定用https://taotoken.net/api。
7.2 加一层打印确认 ObjectId 被正确转换
代码改完后,别直接交给下游,先在终端打印几行确认转换结果:
documents = list(collection.find({}).limit(3)) json_string = dumps(documents, indent=2, ensure_ascii=False) print(json_string[:800]) assert "$oid" not in json_string or "ObjectId" not in str(type(documents[0]["_id"]))如果使用 RELAXED 模式,_id输出为普通十六进制字符串;如果使用 CANONICAL 模式,_id输出为{"$oid": "..."}结构,此时$oid出现是正常的。你要检查的是输出中不应出现类似<bson.objectid.ObjectId object at 0x...>这种 Python 对象表示——出现它说明序列化根本没有走到json_util,代码路径错了。
7.3 回控制台对一下这次调用
配置保存后,先打开 TaoToken 模型对话 用同一把 Key 发一条测试消息,确认模型 ID 和 Base URL 没填错。对话成功后,再回到 Codex 里跑一版转换脚本。打开 Coding Plan 可以看套餐是否够用,Key 的管理入口在 控制台 API Keys。Codex 环境变量的完整对照,可以参考 Claude Code Anthropic 接入文档,虽然那篇以 Claude Code 为主,但里面关于 Base URL 和环境变量的说明对 Codex 同样有参考价值。
8. 最佳实践:从踩坑到顺手
8.1 默认用 json_util,不要跟内置 json 较劲
原文的结论很清楚:能直接用pymongo.json_util就不要自己写JSONEncoder。它由 PyMongo 官方维护,BSON 类型覆盖完整,而且有 RELAXED 和 CANONICAL 两种模式分别应对 API 响应和数据备份。内置json模块的default参数适合临时救急,不适合作为长期方案。Codex 在这一点上也不会给你出馊主意——只要你在 prompt 里写明「用 pymongo.json_util」,它给的代码基本可以直接复制。
8.2 递归处理预留一个口子
即便用了json_util,生产代码里也建议加一层防御性转换。原因很简单:上游数据的字段类型可能在某个版本后发生变化,比如价格从Decimal128变成了字符串,或者嵌套数组里混进了Binary。json_util对这些类型兜底没问题,但一旦遇到它在未来版本中尚未覆盖的新 BSON 类型,报错信息重新变成TypeError。这时把default参数接住:
from bson.decimal128 import Decimal128 def handle_unknown(obj): if isinstance(obj, Decimal128): return str(obj) raise TypeError(f"Unsupported type: {type(obj)}") json_string = dumps( documents, default=handle_unknown, indent=2, json_options=JSONOptions(json_mode=JSONMode.RELAXED) )这段兜底代码不常触发,但线上出问题时有它,日志里能多一行明确的类型信息,而不是突兀的TypeError堆栈。
8.3 游标用完及时关闭连接
原文最后一节专门提醒了client.close()。Codex 生成的代码里可能漏掉这步,粘贴前仔细看一眼。更好的写法是with上下文或try/finally,确保异常时连接也能释放:
client = MongoClient("mongodb://localhost:27017/") try: collection = client["demo_db"]["demo_products"] cursor = collection.find({}) json_string = dumps(list(cursor), indent=2) finally: client.close()如果 MongoDB 跑在远程服务器,连接串里带上合适的serverSelectionTimeoutMS,避免网络抖动时进程长时间卡死。这个参数也可以在 Codex 的 prompt 里顺带要求它加上——它很擅长这类防御性细节。
8.4 跟 mongoexport 差在哪
原文提过mongoexport是更高效的导出工具。实际项目里可以这样分工:一次性全量导出用mongoexport --jsonFormat=relaxed,Python 只负责需要动态拼接查询条件的场景。Codex 也能帮你写mongoexport命令,但命令本身不属于 Python 代码,贴给它的 prompt 要明确说「生成 shell 命令」,否则它会返回subprocess调用方式,绕了一圈反而不直观。
9. 收尾:把这次改造沉淀成习惯
Object of type ObjectId is not JSON serializable这条报错第一次出现时,人的本能反应是搜索「如何把 ObjectId 转字符串」,然后复制一段网上的JSONEncoder代码。这个做法没错,但它只能解决眼前这一条报错。如果项目里多个接口都要输出 MongoDB 文档,更值得做的是一开始就统一用pymongo.json_util,让所有 BSON 类型走同一条序列化通道,配合 Codex 的代码生成能力,把重复劳动降到最低。
TaoToken 在整条链路里的角色就是给 Codex 一个稳定的模型通道:打开 TaoToken 注册、建 Key,把 Base URL 按~/.codex/config.toml和https://taotoken.net/api的格式配对,然后剩下的时间都在解决真正的业务问题,而不是被工具连接问题和序列化边界条件来回打断。拿着报错让 Codex 给方案,落地后回到 TaoToken 控制台对一次调用记录,这套循环跑顺了,JSON 序列化这类问题就不再值得浪费更多时间。