
在处理包含已编码JSON字符串的字典时,直接对外部字典进行JSON序列化会导致内部字符串的引号被二次转义。本文将深入探讨这一常见问题,解释其发生机制,并提供一种清晰的两步解码策略,确保消费者能够正确解析嵌套的JSON数据,尤其适用于消息队列中payload字段被定义为字符串的场景。
在数据传输和存储中,我们经常需要将复杂的数据结构序列化为JSON字符串。一个常见场景是,一个字典中的某个字段本身包含一个已经编码好的JSON字符串。当尝试将这个外部字典再次序列化时,就会遇到内部JSON字符串被“二次转义”的问题。
考虑以下场景:我们有一个消息体 message,它是一个Python字典,需要根据Avro schema将其序列化为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 已定义
avro_schema = {
"type": "record",
"name": "MyMessage",
"fields": [
{"name": "name", "type": "string"},
{"name": "ingestion_ts", "type": {"type": "long", "logicalType": "timestamp-micros"}},
{"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 将 message 序列化为 JSON 字符串
fo = StringIO()
fastavro.json_writer(fo, avro_schema, [message])
message_str = fo.getvalue()
print(f"内部 JSON 字符串: {message_str}")
# 示例输出: '{"name": "any", "ingestion_ts": 1703192665965373, "values": {"amount": 5, "countries": ["se", "nl"], "source": {"name": "web", "url": "whatever"}}}'现在,这个 message_str 需要被包装到一个更大的字典 wrap 中,作为其 payload 字段的值,然后发送到消息队列。消息队列期望的外部Schema如下:
{
"type": "record",
"namespace": "CDCEvent",
"name": "CDCEvent",
"fields": [
{"doc": "The system that generated the event", "type": "string", "name": "sys"},
{"doc": "The operation performed on the event", "type": "string", "name": "op"},
{"doc": "The content of the event", "type": "string", "name": "payload"}
]
}注意,payload 字段的类型被定义为 string。这意味着 payload 字段的值在外部JSON中必须是一个普通的字符串。
如果我们直接将 message_str 赋值给 wrap 字典的 payload 字段,并再次使用 json.dumps 进行序列化:
wrap = {
"sys": "my_system",
"op": "c",
"payload": message_str
}
wrap_str = json.dumps(wrap)
print(f"二次序列化后的 JSON 字符串: {wrap_str}")
# 示例输出: '{"sys": "my_system", "op": "c", "payload": "{\"name\": \"any\", \"ingestion_ts\": 1703192665965373, \"values\": {\"amount\": 5, \"countries\": [\"se\", \"nl\"], \"source\": {\"name\": \"web\", \"url\": \"whatever\"}}}"}'可以看到,payload 字段中的双引号都被反斜杠 转义了。这是因为 json.dumps 在处理 wrap 字典时,将 message_str 视为一个普通的Python字符串值。为了确保生成的外部JSON字符串是合法的,它必须对 message_str 内部的所有特殊字符(如双引号)进行转义。这并非错误,而是 json.dumps 按照JSON规范正确地序列化一个包含字符串值的字段。
Easily find JSON paths within JSON objects using our intuitive Json Path Finder
30
问题的核心不在于生产者端的“二次编码”错误,而在于消费者端如何正确地“二次解码”。如果消费者直接尝试将 wrap_str 整体作为一个JSON对象进行单次解析,并期望 payload 字段的值是一个可以直接使用的JSON对象,那么就会失败,因为它得到的是一个包含转义字符的字符串。
正确的解析方法是采用两步解码策略:
以下是消费者端的示例代码:
# 假设消费者接收到 wrap_str
received_wrap_str = '{"sys": "my_system", "op": "c", "payload": "{\"name\": \"any\", \"ingestion_ts\": 1703192665965373, \"values\": {\"amount\": 5, \"countries\": [\"se\", \"nl\"], \"source\": {\"name\": \"web\", \"url\": \"whatever\"}}}"}'
# 第一步:解析外部 JSON
wrapped_object = json.loads(received_wrap_str)
print(f"第一步解析结果 (Python 字典): {wrapped_object}")
# 此时 wrapped_object['payload'] 的值是一个不含转义符的字符串:
# '{"name": "any", "ingestion_ts": 1703192665965373, "values": {"amount": 5, "countries": ["se", "nl"], "source": {"name": "web", "url": "whatever"}}}'
# 第二步:解析 payload 字段中的内部 JSON 字符串
payload_json_str = wrapped_object["payload"]
print(f"从 payload 字段中取出的 JSON 字符串: {payload_json_str}")
# 根据原始内部 Avro Schema 进行解析
# 注意:这里需要再次提供内部消息的 avro_schema
inner_message_stream = StringIO(payload_json_str)
decoded_messages = []
for record in fastavro.json_reader(inner_message_stream, avro_schema):
decoded_messages.append(record)
print(f"第二步解析结果 (原始消息列表): {decoded_messages}")
# 示例输出: [{'name': 'any', 'ingestion_ts': 1703192665965373, 'values': {'amount': 5, 'countries': ['se', 'nl'], 'source': {'name': 'web', 'url': 'whatever'}}}]通过这种两步解析方法,消费者能够成功地从 wrap_str 中提取出原始的 message 数据。
# 如果外部 Schema 允许 payload 是一个 JSON 对象
# 那么生产者端可以直接这样处理,避免预先序列化
# wrap = {
# "sys": "my_system",
# "op": "c",
# "payload": message # 直接放置字典,而不是字符串
# }
# wrap_str = json.dumps(wrap) # json.dumps 会自动处理嵌套字典在这种情况下,消费者端只需要一步 json.loads(wrap_str) 即可。
当一个字典的字段被明确要求存储一个已经编码的JSON字符串时,对其进行外部JSON序列化是会产生内部引号转义的。这并非错误,而是标准JSON序列化行为。解决之道在于消费者端采用两步解码策略:首先解析外部JSON以获取包含内部JSON字符串的字段值,然后再次解析该字符串以恢复原始数据。理解这一机制对于构建健壮的数据处理管道至关重要。
以上就是处理嵌套JSON字符串的正确姿势:避免二次转义与多层解析的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号