Python JSON 序列化完全指南

Python 阅读约 14 分钟
PythonJSON序列化json模块性能优化

全面掌握 Python JSON 序列化与反序列化技术,涵盖内置 json 模块、自定义编码器、高性能第三方库对比以及生产环境最佳实践。

数据类型映射

Python 的 json 模块支持以下核心类型映射关系。理解这张表是正确使用序列化的基础。Python 的内置类型 JSON 兼容性较好,但仍有不少边界情况需要特殊处理。

Python 类型JSON 类型说明
dictobject键必须是字符串类型
list, tuplearraytuple 反序列化后变为 list
strstring自动处理 Unicode 转义
int, floatnumber含 NaN/Infinity 需特殊处理
True, Falsetrue, false大小写敏感
Nonenull与 JavaScript null 对应

注意 tuple 被序列化为 JSON array,反序列化后变为 list,这是不可逆的转换。如果你的业务逻辑依赖元组的不可变性,需要在反序列化后用 object_hook 手动还原。对于 datetimeDecimalsetfrozensetbytescomplex 等自定义类型,json 模块默认会抛出 TypeError。这意味着在生产环境中,几乎每个涉及日期时间和精确数值的项目都需要自定义编码器。

json.dumps() 核心参数

json.dumps() 是 Python 中最常用的序列化函数,它将 Python 对象转换为 JSON 格式的字符串。掌握它的关键参数可以大幅提升代码的可读性和可维护性。以下代码展示了三个最常用参数的实际效果。

import json

data = {"name": "张三", "city": "北京", "score": 95.5, "tags": ["Python", "JSON"]}

# 默认序列化:紧凑输出,ASCII 转义中文
compact = json.dumps(data)
print(compact)  # {"name": "张三", "city": "北京", ...}

# 美化输出:缩进 + 中文原样 + 键排序
pretty = json.dumps(data, indent=2, ensure_ascii=False, sort_keys=True)
print(pretty)
# {
#   "city": "北京",
#   "name": "张三",
#   "score": 95.5,
#   "tags": [
#     "JSON",
#     "Python"
#   ]
# }

ensure_ascii=False 使中文字符原样输出而不转义为 \uXXXX 格式的 Unicode 转义序列,这在处理中文 JSON 数据时几乎是必须设置的参数。sort_keys=True 按字母序排列键名,便于版本控制和 diff 比较。indent 控制缩进层级,当传入 None0 时输出为一行紧凑格式。此外还有 separators 参数可以进一步控制紧凑度:传入 (',', ':') 可以移除默认的空格分隔符,使输出体积减小约 10% 到 15%,在带宽敏感的场景下非常实用。

json.loads() 与 object_hook

json.loads() 用于将 JSON 字符串解析为 Python 对象。通过 object_hook 参数,可以在反序列化时对每一个 JSON object 执行自定义的转换操作,这是实现类型还原和数据校验的核心机制。

import json
from datetime import datetime

json_str = '{"name": "张三", "age": 28, "created_at": "2026-07-15T10:30:00Z"}'

def custom_object_hook(dct):
    """将 ISO 日期字符串自动还原为 datetime 对象"""
    for key, value in dct.items():
        if isinstance(value, str) and key.endswith("_at"):
            dct[key] = datetime.fromisoformat(value.replace("Z", "+00:00"))
    return dct

result = json.loads(json_str, object_hook=custom_object_hook)
print(result["created_at"])       # 2026-07-15 10:30:00+00:00
print(type(result["created_at"])) # <class 'datetime.datetime'>

object_hook 在 JSON 解析的每一层 object 构建完成后立即调用,这意味着嵌套的 object 会从内到外依次触发钩子。与之对应的 object_pairs_hook 可以拿到保留原始顺序的键值对列表,适用于需要严格保序的场景。一个常见的进阶用法是将 object_hook 与 Python 的 dataclassnamedtuple 配合使用,在反序列化时直接将字典转换为强类型对象,从而在类型安全层面获得更好的保障。

文件读写

处理 JSON 文件时应使用流式接口 json.dump()json.load(),两函数直接操作文件对象,避免将整个文件数据一次性读入内存,尤其适合处理几百 MB 甚至 GB 级别的 JSON 数据。

import json

# 写入文件 —— 注意 encoding 参数
config = {"version": "3.2", "features": {"logging": True, "cache": True}}
with open("config.json", "w", encoding="utf-8") as f:
    json.dump(config, f, indent=2, ensure_ascii=False)

# 读取文件 —— 自动检测编码
with open("config.json", "r", encoding="utf-8") as f:
    loaded = json.load(f)

print(loaded["features"]["logging"])  # True

文件操作务必指定 encoding="utf-8",否则在 Windows 平台可能默认使用系统编码(如 GBK),导致中文乱码或被破坏。json.dump()json.load() 内部使用了迭代式的编码和解码策略,内存占用和文件大小成线性关系,但显著低于使用 json.dumps() 拼接字符串再一次性写入的方式。对于超大 JSON 文件(例如日志聚合数据),建议进一步使用 ijson 等流式解析库,逐个 yield 解析出的元素而非加载整个结构。

自定义 JSONEncoder 子类

对于 datetimeDecimal 等非标准类型,需要继承 JSONEncoder 并重写 default() 方法来实现类型扩展。

import json
from datetime import datetime
from decimal import Decimal

class CustomEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime):
            return obj.isoformat()
        if isinstance(obj, Decimal):
            return float(obj)
        if isinstance(obj, set):
            return list(obj)
        if isinstance(obj, complex):
            return {"real": obj.real, "imag": obj.imag}
        return super().default(obj)

data = {
    "created": datetime(2026, 7, 15, 10, 30, 0),
    "price": Decimal("19.99"),
    "tags": {"python", "json"},
    "vector": 3 + 4j
}

result = json.dumps(data, cls=CustomEncoder, ensure_ascii=False)
print(result)
# {"created": "2026-07-15T10:30:00", "price": 19.99,
#  "tags": ["python", "json"], "vector": {"real": 3.0, "imag": 4.0}}

必须调用 super().default(obj) 以确保未处理的类型能正确抛出 TypeError,而不是被静默吞掉导致难以排查的 bug。对于高频调用的序列化场景,可以考虑将类型分发逻辑提取到类属性字典中,用 type(obj) 作为键进行 O(1) 查找,避免连续多个 isinstance 检查带来的开销。另一个替代方案是使用 functools.singledispatch 实现可扩展的分派逻辑。

常见陷阱与解决方案

非 ASCII 字符转义、循环引用和特殊浮点值是 Python JSON 开发中最容易踩的三个坑,每个都有其深层原因和标准应对方法。

import json

# 陷阱1: ensure_ascii 默认 True —— 中文变乱码
data = {"message": "你好世界"}
print(json.dumps(data))  # {"message": "你好世界"}
# 必须显式设置 ensure_ascii=False

# 陷阱2: 循环引用抛出 RecursionError
class Node:
    def __init__(self, name):
        self.name = name
        self.parent = None

root = Node("root")
child = Node("child")
root.parent = child  # 形成引用环
child.parent = root
# json.dumps(root.__dict__)  # RecursionError!

# 陷阱3: NaN/Infinity 在严格 JSON 中非法
bad_math = {"value": float("nan"), "score": float("inf")}
print(json.dumps(bad_math))           # {"value": NaN, "score": Infinity}
print(json.dumps(bad_math, allow_nan=False))  # 抛出 ValueError

针对循环引用,可以维护一个已访问对象集合,当检测到重复时输出占位符表示引用关系。也可以使用 jsonpickle 库自动处理对象图,它会在输出中添加 py/objectpy/ref 元数据来追踪引用。NaN 和 Infinity 不是合法 JSON 值,绝大多数 JSON 解析器会拒绝它们,生产环境应当启用 allow_nan=False 并前置处理异常数值,将其替换为 null 或合理的哨兵值。

性能对比:json vs ujson vs orjson

标准库 json 基于纯 Python 实现,代码可读性强但性能并非最优选择。对于高吞吐量场景,社区提供了多种 C 扩展实现。

import json, ujson, orjson, time

data = {"items": [{"id": i, "name": f"item_{i}", "value": i * 1.5}
        for i in range(10000)]}

# json(标准库)— 纯 Python,兼容性最好
t0 = time.perf_counter()
std_result = json.dumps(data)
t1 = time.perf_counter()
print(f"json:     {(t1 - t0) * 1000:.1f} ms")

# ujson — C 实现,API 类似但细节有差异
t0 = time.perf_counter()
ujson_result = ujson.dumps(data)
t1 = time.perf_counter()
print(f"ujson:    {(t1 - t0) * 1000:.1f} ms")

# orjson — 速度最快,输出 bytes 类型
t0 = time.perf_counter()
orjson_result = orjson.dumps(data)
t1 = time.perf_counter()
print(f"orjson:   {(t1 - t0) * 1000:.1f} ms")

orjson 通常比标准库快 5 到 10 倍,ujson 大约快 2 到 4 倍。orjson 的一大优势在于默认按 key 排序输出,并且对 datetimeUUID 有原生支持,无需自定义编码器。但它返回 bytes 而非 str,需要 .decode() 后才能用于字符串场景。ujsonensure_ascii 的控制不如标准库灵活。在微服务架构中,如果 JSON 序列化占用了显著的 CPU 时间,更换库可能是投入产出比最高的优化手段之一。

安全实践

json 模块只解析纯 JSON 结构,不会执行任意 Python 代码,这比 pickle 安全得多。但配合 object_hook 等回调机制时仍然需要防范潜在风险。

import json

MAX_PAYLOAD_BYTES = 1 * 1024 * 1024   # 1MB
MAX_DEPTH = 50
MAX_KEYS = 1000

def safe_parse(user_input: str):
    # 第一层防护:大小限制
    if len(user_input.encode("utf-8")) > MAX_PAYLOAD_BYTES:
        raise ValueError("输入数据过大")

    # 第二层防护:解析异常处理
    try:
        data = json.loads(user_input)
    except json.JSONDecodeError as e:
        raise ValueError(f"无效的 JSON 格式,位置 {e.pos}: {e.msg}") from e

    # 第三层防护:基本结构校验
    if not isinstance(data, (dict, list)):
        raise ValueError("只接受 JSON object 或 array")

    # 第四层防护:深度和键数量限制(防止递归炸弹)
    if isinstance(data, dict) and len(data) > MAX_KEYS:
        raise ValueError("键数量超出限制")

    return data

对来自外部 API 的 JSON 响应同样要做好防御性编程:使用 dict.get() 代替直接下标访问;使用 pydanticdataclasses 定义严格的 Schema 在反序列化后立即校验,不符合预期的字段直接拒绝;对 object_hook 内部逻辑做输入长度和类型检查,防止注入攻击。

各语言 JSON API 对比

语言 序列化 反序列化 美化输出 配置方式
Python json(内置) json.dumps(obj) json.loads(str) json.dumps(obj, indent=2) ensure_ascii, default, cls
JavaScript JSON(内置) JSON.stringify(obj) JSON.parse(str) JSON.stringify(obj, null, 2) replacer, space
Java Jackson / Gson mapper.writeValueAsString(obj) mapper.readValue(str, Class.class) mapper.writerWithDefaultPrettyPrinter() 注解、Module、Feature
Go encoding/json(内置) json.Marshal(obj) json.Unmarshal(data, &obj) json.MarshalIndent(obj, "", " ") struct tag
C# System.Text.Json(内置) JsonSerializer.Serialize(obj) JsonSerializer.Deserialize&lt;T&gt;(str) WriteIndented = true JsonSerializerOptions

常见问题 (FAQ)

JSON 序列化时如何处理日期时间类型?

大多数语言的 JSON 库默认不支持日期时间类型。通常做法是序列化为 ISO 8601 格式字符串(如 "2026-07-15T10:30:00Z")或 Unix 时间戳,反序列化时再转换回日期时间对象。Python 可通过 default 参数指定自定义编码器;JavaScript 可在 toJSON 方法中处理;Java 的 Jackson 支持 @JsonFormat 注解配置日期格式。

如何忽略 JSON 序列化中的 null 值或空字段?

不同语言的实现方式不同:Python 可在自定义编码器中过滤 None 值;JavaScript 可使用 replacer 函数或 JSON.stringify 的第二个参数;Java Jackson 用 @JsonInclude(Include.NON_NULL) 注解;Go 使用 omitempty struct tag;C# 设置 DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull。

JSON 序列化时如何处理循环引用?

循环引用是 JSON 序列化的常见陷阱。标准 JSON 库通常会抛出异常或栈溢出。解决方案包括:使用 @JsonIdentityInfo 注解(Java Jackson)、实现自定义序列化器跳过反向引用、将对象图转换为 DTO 后再序列化,或使用专门的图序列化库如 flatted(JavaScript)。

为什么我的私有字段没有被序列化?

JSON 序列化库通常只序列化公开(public)字段或带有 getter 的属性。这是设计上的安全考虑。Python 的 json 模块默认只序列化 dict 的公共键;Java Jackson 可通过 @JsonProperty 注解私有字段或配置 ObjectMapper 的 Visibility;C# 中 System.Text.Json 默认只序列化公共属性,需通过 [JsonInclude] 特性包含非公共成员。

JSON 序列化性能有哪些优化技巧?

1) 复用序列化器实例(避免每次创建 ObjectMapper/JsonSerializerOptions);2) 对于大型数据,使用流式 API(如 Jackson 的 JsonGenerator / Python 的 iterencode);3) 使用编译时生成(C# Source Generator、Go easyjson);4) 避免过多嵌套层次;5) 数值类型避免不必要的大精度;6) 使用数据库分页或分块传输大 JSON。

推荐工具

以下工具可以帮助你在 Python 开发中更高效地处理 JSON 数据:

相关文章