开始前,会这些就够了
- 认识 JSON 对象中的字段名和值
- 知道工具执行前需要校验参数
- 先读 AI把参数填对了,为什么还不能执行?:先分清结构规则与业务规则,再看更早的文本解析边界。
- 先读 字段叫 a
/b,为什么工具找成了 a 里面的 b?:先熟悉字段名的编码与解码,避免只看原始字符判断名字。
遇到不熟悉的词,先看这 2 个解释
- 重复成员名
- 同一个 JSON 对象里,同一个解码后的字段名被写了不止一次。
- object_pairs_hook
- Python JSON 解析器给你的一个入口,让你在字段对合并成字典前检查它们。
工具参数写成 {"amount":100,"amount":900}。你肉眼看见两个金额,Python 默认解析结果却只有 amount=900。把两个成员的顺序调过来,结果又变成 100。
这篇实验只研究一个很早的边界:原始文本变成对象时,哪些信息已经消失。把规则写得再详细,也无法从只剩一个字段的对象里重新读出被覆盖的第一个金额。
把例子带到自己的电脑上
下载完整练习包
需要 Python 3.10 或更新版本,无第三方依赖;实测 Windows / Python 3.14.0,Python 3.10 仅做语法兼容检查。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。
下载 ZIP · 5 个文件包里有什么
- demo.py:完整可运行源码
- README.md:运行步骤与练习
- observed.stdout.txt:本次实际输出
- verification.json:源码和运行验证记录
- SHA256SUMS.txt:文件摘要
在这个文件夹打开 PowerShell,逐行运行
python --version
python -B demo.py解压后,在含 demo.py 的目录打开终端。命令分别回车执行。代码实际执行 Python JSON 解析,覆盖普通重复名、转义后相同名字、嵌套重复、相同值重复,以及不同对象中的同名成员;不调用任何 AI 模型或外部工具。形状检查是五行普通 Python 条件,不是 JSON Schema 库。
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
两个 amount 经过解析,还剩几个?
正在准备实验控件…
浏览器真实执行 JSON.parse。重复检查只检查这个构造器保留的字段序列,不是任意 JSON 的通用检测器;下方 Python 示例才在解析时处理完整嵌套对象。实验不调用工具或发送交易。
先用两行文本看见丢失的东西
文本 A 是 {"amount":100,"amount":900},文本 B 是 {"amount":900}。Python 的默认 json.loads 对它们都得到 {"amount": 900}。两份原文不同,解析结果却相同;仅拿到这个结果的函数无法判断自己收到的是 A 还是 B。
假设后面的形状检查只要求有一个 amount,值是 1 到 1000 之间的整数。两个解析结果都会通过。这里没有说金额 900 一定可以执行业务;只是说“这一层检查通过”并没有证明“原文从未重复字段”。
相同问题也发生在相同值上:{"amount":100,"amount":100} 仍然出现两次名字。是否拒绝这种输入,应当由接收方明确制定策略,不能依赖两个值碰巧相等。
标准建议名字唯一,但不要把建议说成语法禁令
RFC 8259 第 4 节建议同一对象内的成员名保持唯一,使用的是 SHOULD。它同时说明,遇到重复名时,各实现可能保留最后一个、报错,或保留全部。因此不能把“有重复名”简单说成“违反 JSON 语法、所有解析器必定拒绝”。
Python 3.14 文档明确说明默认解码器接受重复名并保留最后一对。浏览器实验也直接执行 JSON.parse,展示它对本页构造输入的结果。这里不把某一种实现的表现当作所有语言的共同承诺。
名字比较还要发生在字符串解码之后。例如 "amount" 和 "am\u006funt" 写法不同,后者的 \u006f 解码为字母 o,最终都是 amount。用原始文本的字面字符串去重会漏掉这一组。
把重复检查放在成员对变成字典之前
Python 的 object_pairs_hook 接到的是当前 JSON 对象的有序“名字、值”成员对列表。依次检查已出现的解码后名字,遇到重复立即抛错,再把无重复的成员放进字典。这个钩子也会用于嵌套对象。
相反,object_hook 接到的已经是字典。示例中它看到的成员数是 1,无法知道原来写过两次。先 json.loads,再检查 dict.keys() 是否重复,也会遇到同样的时机问题。
同名不等于重复:{"a":{"amount":1},"b":{"amount":2}} 的两个 amount 分别属于两个对象,可以同时存在。本例检查的是每个对象内部,不是把整份文档中所有名字混在一起。
运行完整小例子
代码实际执行 Python JSON 解析,覆盖普通重复名、转义后相同名字、嵌套重复、相同值重复,以及不同对象中的同名成员;不调用任何 AI 模型或外部工具。形状检查是五行普通 Python 条件,不是 JSON Schema 库。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
"""Run with Python 3.14; standard library only; no external tool calls."""
import json
def reject_duplicate_names(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ValueError(f"duplicate member: {key}")
result[key] = value
return result
def load_unique(text):
# This adds a duplicate-name policy, not every possible JSON policy.
return json.loads(text, object_pairs_hook=reject_duplicate_names)
def rejects(text):
try:
load_unique(text)
except ValueError as exc:
return str(exc)
raise AssertionError("expected a duplicate-name rejection")
def main():
raw = '{"amount":100,"amount":900}'
escaped = r'{"amount":100,"am\u006funt":900}'
reversed_raw = '{"amount":900,"amount":100}'
ordinary = json.loads(raw)
# This is a tiny shape check, not a JSON Schema implementation.
shape_ok = (set(ordinary) == {"amount"}
and type(ordinary["amount"]) is int
and 1 <= ordinary["amount"] <= 1000)
print("default amount:", ordinary["amount"])
print("reversed amount:", json.loads(reversed_raw)["amount"])
print("parsed shape check:", shape_ok)
print("duplicate policy:", rejects(raw))
print("escaped-name policy:", rejects(escaped))
print("nested policy:", rejects('{"request":{"amount":1,"amount":2}}'))
print("same-value policy:", rejects('{"amount":100,"amount":100}'))
separate = load_unique('{"a":{"amount":1},"b":{"amount":2}}')
print("different objects:", separate["a"]["amount"], separate["b"]["amount"])
seen_by_object_hook = []
json.loads(raw, object_hook=lambda obj: seen_by_object_hook.append(len(obj)))
print("members visible to object_hook:", seen_by_object_hook)
assert ordinary == {"amount": 900} and shape_ok
assert json.loads(escaped) == ordinary
assert json.loads(reversed_raw) == {"amount": 100}
assert separate == {"a": {"amount": 1}, "b": {"amount": 2}}
assert seen_by_object_hook == [1]
print("checks: passed")
if __name__ == "__main__":
main()
怎样运行:在独立练习文件夹中,依次执行下载区的命令。2026-10-26 使用 Python 3.10 或更新版本,无第三方依赖;实测 Windows / Python 3.14.0,Python 3.10 仅做语法兼容检查 复跑这份源码并核对完整输出。代码实际执行 Python JSON 解析,覆盖普通重复名、转义后相同名字、嵌套重复、相同值重复,以及不同对象中的同名成员;不调用任何 AI 模型或外部工具。形状检查是五行普通 Python 条件,不是 JSON Schema 库。
运行后对照结果
default amount: 900
reversed amount: 100
parsed shape check: True
duplicate policy: duplicate member: amount
escaped-name policy: duplicate member: amount
nested policy: duplicate member: amount
same-value policy: duplicate member: amount
different objects: 1 2
members visible to object_hook: [1]
checks: passed接入工具流程时,明确每一层负责什么
如果你掌握原始工具参数文本,可以先按协议的重复名策略解析,再做结构、取值和业务检查。日志、预览与执行最好消费同一个已经验证的结果,避免各层对原文采用不同解释。
如果上游 SDK 只把解析后的对象交给你,就不能靠这个对象证明原文没有重复。需要在能看见原始成员的入口实施策略,或者确认上游已有等价保证;不要假装后面的 schema 会补回消失的信息。
本例只增加“拒绝同一对象内重复名”这一条策略。它不是完整 JSON 安全接入层,也没有处理所有数字扩展、资源大小限制或业务授权。Python 的 strict=True 主要涉及字符串控制字符,并不是开启重复名拒绝的开关。
记住这三点
- 重复名可能在文本转对象时被覆盖;解析后的校验无法还原被丢弃的原始成员。
- 检查应比较解码后的名字,并按每个对象分别处理;不同对象的同名字段不冲突。
object_pairs_hook保留检查时机,object_hook接收字典时已经晚了一步。
别把例子用过头
- 把 RFC 的 SHOULD 唯一改写成“重复字段绝对不是合法 JSON 语法”。
- 用正则或原文子串查找代替真正的字符串解码与对象边界解析。
- 认为相同值的重复名、strict=True,或解析后的 schema 校验自动解决了重复策略。
自己试一小步
不运行代码,先预测哪一层还有足够信息发现重复。
- 给
object_pairs_hook和object_hook分别打印收到的内容。 - 把第二个字段名改为转义写法,再把重复字段移到数组中的嵌套对象里。
- 构造两份不同原文,使默认解码后的字典完全相同,并解释为何后置检查无法区分。
做到这些,就算完成
- 说清你检查的是原始文本、成员对列表,还是已合并的字典。
- 重复策略必须在同一对象内部生效;不同对象中的同名字段应通过。
- 为每个预期写一个断言,并保留原文与解析结果的对照。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- RFC 8259 — The JavaScript Object Notation (JSON) Data Interchange Format (新标签页)↗
第 4 节的名字唯一性建议、重复名互操作性风险,以及第 8.3 节解码后字符串比较。
- Python 3.14 — json: JSON encoder and decoder (新标签页)↗
默认重复名保留最后一对;
object_pairs_hook与object_hook的输入区别,以及 strict 参数的范围。
找一份资料继续练
先选一项,按下面的范围继续学;不必从头读完整门课程。
fastjson2 中文:同一个字段出现两次,最后读到哪个值 (新标签页)↗
作为 Java 对照,只看固定 2.0.61 版 DuplicateKeyValueAsArray 的含义,比较“保留多个值”和本文“遇到重复即拒绝”两种策略。新的跨语言对照留作练习,不把 Python 输出当作 fastjson2 实测。
查看学习准备与访问说明 →