Python JSON 序列化:处理嵌套 JSON 字符串的正确姿势

碧海醫心
发布: 2025-11-05 11:16:09
原创
838人浏览过

Python JSON 序列化:处理嵌套 JSON 字符串的正确姿势

本文探讨了在 python 中将已编码json 字符串作为字典字段值进行二次序列化时,出现字符转义的常见问题。核心观点是,这种看似“双重编码”的现象并非错误,而是标准行为。关键在于消费者需要执行对称的两步解码过程:首先解析外部 json 结构,然后将内部的 json 字符串再次解析,以正确还原原始数据。

在数据传输和存储场景中,我们经常需要将复杂的数据结构序列化为 JSON 字符串。一个常见的场景是,一个字典中的某个字段本身就包含一个已经 JSON 编码的字符串。当尝试对包含这种字段的整个字典进行二次 JSON 编码时,我们可能会观察到内部 JSON 字符串中的双引号被转义(例如 "),这常常引起困惑,被误认为是“双重编码”的错误。然而,这实际上是 json.dumps 函数在处理字符串值时的标准行为,并且可以通过正确的解码策略来解决。

场景描述与问题分析

考虑一个初始的 Python 字典,我们可能使用 fastavro 或 json.dumps 将其序列化为 JSON 字符串。

import json
from datetime import datetime
from io import StringIO
import fastavro

# 原始数据字典
message = {
    "name": "any",
    "ingestion_ts": datetime.utcnow(),
    "values": {
        "amount": 5,
        "countries": ["se", "nl"],
        "source": {
            "name": "web",
            "url": "whatever"
        }
    }
}

# 假设 avro_schema 已定义,用于 fastavro 编码
avro_schema = """
{
  "type": "record",
  "name": "MyMessage",
  "fields": [
    {"name": "name", "type": "string"},
    {"name": "ingestion_ts", "type": {"type": "long", "logicalType": "timestamp-millis"}},
    {"name": "values", "type": {
      "type": "record",
      "name": "Values",
      "fields": [
        {"name": "amount", "type": "int"},
        {"name": "countries", "type": {"type": "array", "items": "string"}},
        {"name": "source", "type": {
          "type": "record",
          "name": "Source",
          "fields": [
            {"name": "name", "type": "string"},
            {"name": "url", "type": "string"}
          ]
        }}
      ]
    }}
  ]
}
"""

# 使用 fastavro 将字典编码为 JSON 字符串
fo = StringIO()
fastavro.json_writer(fo, json.loads(avro_schema), [message])
message_str = fo.getvalue()
print(f"首次编码结果 (message_str):
{message_str}
")
# 示例输出: '{"name": "any", "ingestion_ts": 1703192665965373, "values": {"amount": 5, "countries": ["se", "nl"], "source": {"name": "web", "url": "whatever"}}}'
登录后复制

现在,我们需要将这个 message_str 字符串嵌入到一个新的字典中,作为 payload 字段的值,然后再次对其进行 JSON 序列化。

# 外部包装字典,模拟消息队列的期望格式
wrap = {
    "sys": "my_system",
    "op": "c",
    "payload": message_str # payload 字段的值是一个已编码的 JSON 字符串
}

# 对外部字典进行二次 JSON 编码
wrap_str = json.dumps(wrap)
print(f"二次编码结果 (wrap_str):
{wrap_str}
")
# 示例输出: '{"sys": "my_system", "op": "c", "payload": "{\"name\": \"any\", \"ingestion_ts\": 1703192665965373, \"values\": {\"amount\": 5, \"countries\": [\"se\", \"nl\"], \"source\": {\"name\": \"web\", \"url\": \"whatever\"}}}"}'
登录后复制

从 wrap_str 的输出中可以看出,payload 字段的值被包裹在额外的双引号中,并且内部的原始双引号被转义成了 "。这并非 json.dumps 的错误,而是其正确处理字符串字面量的方式。当 json.dumps 遇到一个字符串值时,它会确保该字符串在最终的 JSON 输出中被正确地表示为一个 JSON 字符串。这意味着任何特殊字符(如双引号、反斜杠等)都需要被转义,以区分字符串内容和 JSON 结构本身。

立即学习Python免费学习笔记(深入)”;

Find JSON Path Online
Find JSON Path Online

Easily find JSON paths within JSON objects using our intuitive Json Path Finder

Find JSON Path Online 30
查看详情 Find JSON Path Online

正确的解码策略

解决“双重编码”问题的关键在于理解和实施对称的解码过程。如果数据经过了两次 JSON 编码,那么消费者也应该执行两次 JSON 解码。

# 模拟消费者接收到 wrap_str
received_wrap_str = wrap_str

# 第一步解码:解析外部 JSON 结构
wrapped_object = json.loads(received_wrap_str)
print(f"第一步解码结果 (wrapped_object):
{wrapped_object}
")

# 获取 payload 字段的值,它仍然是一个 JSON 字符串
payload_json_str = wrapped_object["payload"]
print(f"从 wrapped_object 获取的 payload 字符串:
{payload_json_str}
")

# 第二步解码:解析内部的 JSON 字符串
# 如果原始数据是 fastavro 编码的,则使用 fastavro.json_reader
# 如果原始数据是 json.dumps 编码的,则使用 json.loads
decoded_payload = []
for record in fastavro.json_reader(StringIO(payload_json_str), json.loads(avro_schema)):
    decoded_payload.append(record)

print(f"第二步解码结果 (decoded_payload):
{decoded_payload}
")

# 验证解码结果是否与原始 message 匹配
# 注意:datetime 对象因为毫秒精度或时区差异,其时间戳值可能略有不同,
# 此处仅作结构和主要内容的比较以验证数据还原。
assert decoded_payload[0]["name"] == message["name"]
assert decoded_payload[0]["values"]["amount"] == message["values"]["amount"]
print("数据成功还原!")
登录后复制

通过上述两步解码,我们可以完全还原原始的数据结构,证明了这种“双重编码”并非问题,而是 JSON 规范下处理嵌套字符串的正常行为。

注意事项与最佳实践

  1. 编码与解码的对称性:这是处理嵌套序列化数据的核心原则。如果数据在发送前经过 N 次序列化,那么在接收端就必须经过 N 次反序列化才能恢复原始数据。
  2. 明确数据类型:在设计数据传输协议或 API 时,务必清晰地指出某个字段是 JSON 字符串,而不是一个直接的 JSON 对象。例如,如果目标 schema 明确规定 payload 字段的类型是 string,那么将一个已编码的 JSON 字符串作为其值是完全符合预期的。
  3. 避免不必要的序列化/反序列化:如果 payload 字段的接收者期望的是一个 JSON 对象(即 Python 字典/列表),那么在包装时就不应该提前将其序列化为字符串。而应该直接将 Python 字典赋值给 payload 字段,让 json.dumps 在最后一步统一处理。
    # 示例:如果消费者期望 payload 是一个 JSON 对象
    # 假设原始 message 是一个 Python 字典,且外部 schema 定义 payload 为对象类型
    # wrap_correct_for_object_payload = {
    #     "sys": "my_system",
    #     "op": "c",
    #     "payload": message # 直接放入 Python 字典对象
    # }
    # wrap_str_final = json.dumps(wrap_correct_for_object_payload)
    # 此时,message 会被 json.dumps 自动编码为 JSON 对象,不会出现转义问题。
    登录后复制

    然而,在本文的场景中,外部 schema 明确要求 payload 是 string 类型,因此将已编码的 JSON 字符串赋值给 payload 是正确的做法。

  4. 文档化:对于复杂的嵌套数据结构,务必清晰地文档化其编码和解码流程,特别是对于不同层级的数据可能采用不同序列化方式(如 Avro JSON 与标准 JSON)的情况,这对于消费者正确解析数据至关重要。

总结

当 Python 字典的一个字段需要包含一个已编码的 JSON 字符串时,对其进行二次 JSON 序列化会导致内部双引号被转义。这并非错误,而是 json.dumps 处理字符串字面量的标准行为。解决此问题的关键在于消费者需要执行对称的两步解码过程:首先使用 json.loads 解析外部 JSON 结构,然后从结果中提取内部的 JSON 字符串,并再次使用相应的解码器(如 json.loads 或 fastavro.json_reader)进行解析。理解并遵循这一原则,可以有效地处理嵌套 JSON 字符串的编码与解码问题,确保数据传输的完整性和正确性。

以上就是Python JSON 序列化:处理嵌套 JSON 字符串的正确姿势的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号