news 2026/9/16 12:09:55

PyMongo Cursor 转 JSON 报 ObjectId 错?TaoToken 这样改 Codex 的通道再查 json_util

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PyMongo Cursor 转 JSON 报 ObjectId 错?TaoToken 这样改 Codex 的通道再查 json_util

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_providerbase_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变成了字符串,或者嵌套数组里混进了Binaryjson_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.tomlhttps://taotoken.net/api的格式配对,然后剩下的时间都在解决真正的业务问题,而不是被工具连接问题和序列化边界条件来回打断。拿着报错让 Codex 给方案,落地后回到 TaoToken 控制台对一次调用记录,这套循环跑顺了,JSON 序列化这类问题就不再值得浪费更多时间。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 12:09:33

飞秒激光与金属相互作用:双温模型与MATLAB数值模拟

1. 飞秒激光与金属相互作用的基础物理模型飞秒激光与金属相互作用是一个典型的非平衡态热力学过程。当超短脉冲激光&#xff08;通常脉宽在10-100飞秒量级&#xff09;照射金属表面时&#xff0c;光子能量首先被电子吸收&#xff0c;由于电子-声子耦合时间尺度&#xff08;约1皮…

作者头像 李华
网站建设 2026/9/16 12:09:08

Pentagi:基于Neo4j与轻量AI Agent的攻击链认知建模系统

1. 项目概述&#xff1a;Pentagi 是什么&#xff1f;它解决的不是“渗透测试自动化”&#xff0c;而是“攻击链认知建模”的根本问题Pentagi 这个名字乍看像拼写错误&#xff0c;实则暗藏玄机——它由Penetration Tagi&#xff08;源自拉丁语tactus&#xff0c;意为“触达”“连…

作者头像 李华
网站建设 2026/9/16 12:08:50

高温结构强度与蠕变寿命仿真技术解析

1. 高温结构强度与蠕变寿命仿真概述在航空航天、能源化工等工业领域&#xff0c;高温环境下的结构强度与蠕变寿命评估一直是工程设计中的关键难题。当金属材料长期暴露在高温环境中&#xff0c;即使承受的应力远低于其屈服强度&#xff0c;也会因蠕变效应逐渐产生塑性变形&…

作者头像 李华
网站建设 2026/9/16 12:07:15

免费AI编程助手横评:提升开发效率的关键工具

1. 零成本编程时代的AI助手革命去年帮朋友公司做技术审计时&#xff0c;发现他们开发团队的人均代码产出量突然提升了47%。追问之下才知道&#xff0c;团队给每个程序员都配了AI编程助手。这个数据让我开始系统性测试市面上主流的免费AI编程工具&#xff0c;于是有了这篇横评。…

作者头像 李华
网站建设 2026/9/16 12:06:53

生成式AI营销工具评测:鸿创云在安徽市场的表现

1. 项目背景与行业现状生成式AI技术正在深刻改变数字营销行业的游戏规则。根据最新行业报告显示&#xff0c;2024年第一季度&#xff0c;采用生成式AI技术的营销团队平均内容产出效率提升了3-5倍&#xff0c;而获客成本降低了40%左右。在这样的背景下&#xff0c;各类AI营销工具…

作者头像 李华
网站建设 2026/9/16 12:06:41

SSM框架实现高校毕业设计管理系统开发指南

1. 项目背景与核心价值这个基于SSM框架的Java毕业设计项目管理系统&#xff0c;是专门为高校计算机相关专业学生设计的毕业设计全流程管理工具。我在指导学生的过程中发现&#xff0c;每年毕业季都会出现选题混乱、进度失控、文档丢失等问题&#xff0c;而这个系统正是为了解决…

作者头像 李华