Python通过自定义JSONEncoder处理set或bytes类型序列化

蓝鲸
蓝鲸
发布时间:2026-10-10 | 1人浏览过
原创

在 Python 开发中,标准库 json 模块默认只支持对字典、列表、字符串、整数、浮点数、布尔值以及 None 进行序列化。当待转换的数据结构中包含 set(集合)或 bytes(字节串)对象时,直接调用 json.dumps() 会抛出 TypeError: Object of type ... is not JSON serializable 异常。解决该问题的标准且高内聚方案是:继承 json.JSONEncoder 类并重写 default() 方法,在其中针对集合与字节对象提供专门的类型转换规则,最后在调用 json.dumps() 时通过 cls 参数指定该自定义编码器。

一、默认序列化机制与类型报错原因

JSON 是一种通用数据交换格式,其标准规范仅定义了有限的数据类型抽象:对象(Object)、数组(Array)、字符串(String)、数值(Number)、布尔值(Boolean)以及空值(Null)。Python 的 json 模块严格依据该映射规范设计。对于非标准类型的原生对象,内置编码器无法预设转换目标,因而选择直接抛出异常中断执行。

例如,一个包含去重 ID 集合或二进制哈希摘要的字典:

import json

payload = {
    "status": "active",
    "user_ids": {101, 102, 103},       # set 类型
    "token_bytes": b"secure_auth_token" # bytes 类型
}

# 直接序列化将立即抛出 TypeError
# json.dumps(payload)

对 set 而言,JSON 规范中不存在集合概念,通常应转化为数组(JSON Array);对 bytes 而言,二进制数据可能为 UTF-8 编码的纯文本,也可能为任意二进制图像或加密流,无法统一假设。因此必须由开发者根据具体业务定义转换规则。

Python通过自定义JSONEncoder处理set或bytes类型序列化
图 1:Python 基础类型与 JSON 规范的映射断层及解决方案

二、继承 JSONEncoder 重写 default 方法的具体实现

在 json 模块的设计中,当编码器遇到不能识别的对象时,会调用自身的 default(self, o) 方法。该方法设计之初就是用于扩展支持额外类型的钩子函数(Hook)。开发者只需继承 json.JSONEncoder,覆盖该方法并针对特定类型返回 JSON 原生可序列化的对象即可。

重写 default 方法时需遵循四个关键步骤:

  1. 继承基类:定义继承自 json.JSONEncoder 的子类。
  2. 匹配目标类型:通过 isinstance(o, set) 或 isinstance(o, bytes) 进行类型检测。
  3. 实施数据转换:对于集合,转换为 list;对于字节流,转换为 UTF-8 字符串或 Base64 编码字符串。
  4. 回退兜底调用:对于未匹配的其他未知类型,必须显式调用 super().default(o),以保留基类正常的异常抛出行为。
Python通过自定义JSONEncoder处理set或bytes类型序列化
图 2:自定义 JSONEncoder 实现与方法拦截处理流程

下面是生产环境中推荐的标准编码器实现代码:

import json
import base64

class SafeCustomEncoder(json.JSONEncoder):
    """
    自定义 JSON 编码器,扩展支持 set 与 bytes 类型
    """
    def default(self, obj):
        # 1. 处理集合类型:转为列表
        if isinstance(obj, (set, frozenset)):
            return list(obj)

        # 2. 处理字节串类型:优先 UTF-8 解码,失败则回退至 Base64 编码
        if isinstance(obj, (bytes, bytearray)):
            try:
                return obj.decode("utf-8")
            except UnicodeDecodeError:
                return base64.b64encode(obj).decode("ascii")

        # 3. 对其余不支持类型,交给父类抛出标准 TypeError
        return super().default(obj)

# 验证数据
test_data = {
    "tags": {"python", "json", "web"},
    "text_bytes": "中文测试".encode("utf-8"),
    "binary_bytes": b"\x89PNG\r\n\x1a\n\x00\x00"  # 任意二进制(PNG头)
}

# 调用序列化并指定编码器类
json_output = json.dumps(test_data, cls=SafeCustomEncoder, ensure_ascii=False, indent=2)
print(json_output)

三、set 与 bytes 转换策略的深度对比与考量

虽然将 set 转为 list、将 bytes 转为 str 能够解决报错问题,但在不同的业务情境下,具体策略的选择对数据一致性、可读性以及存储体积有着显著影响。

1. 集合类型:普通转换 vs 排序转换

Python 集合是基于哈希表的无序容器。在同一进程内多次转换,或者在启用了 Python 哈希随机化(Hash Seed Randomization)的不同进程之间转换时,list(set_obj) 产生的列表元素顺序是不固定的。

  • 适用场景对比:如果业务只关心内容本身(如只读查询输出),直接 list(obj) 性能最高;但如果序列化结果用于计算签名、哈希校验、单元测试断言或分布式缓存对比,则必须采用 sorted(list(obj))(要求元素支持比较),以确保序列化结果的确定性(Deterministic Output)。

2. 字节串类型:UTF-8 解码 vs Base64 编码

字节串在底层是纯粹的 8 位字节序列。将其转换为 JSON 字符串时,必须明确其所代表的数据语义:

  • UTF-8 解码策略:适用于原始数据本就是文本内容(例如从套接字接收到的 UTF-8 协议数据、加密签名前的原文字符串)。优点是序列化后的 JSON 具有完全的可读性,便于前端直接显示;缺点是遇到非文本二进制时会触发解码异常。
  • Base64 编码策略:适用于任意二进制文件流(如图片二进制、AES 加密密文、哈希摘要)。优点是通用稳定,绝不损坏任何二进制位;代价是数据体积膨胀约 33%(4 字节表示 3 字节数据),且可读性较差。
Python通过自定义JSONEncoder处理set或bytes类型序列化
图 3:set 与 bytes 序列化转换策略的形态与适用场景对比

四、类继承方案与 default 参数回调的差异

在 Python 中,除了编写子类继承 json.JSONEncoder 外,json.dumps() 还提供了一个便捷的 default 参数,允许直接传入一个处理函数:

def simple_default_handler(o):
    if isinstance(o, set):
        return list(o)
    if isinstance(o, bytes):
        return o.decode("utf-8", errors="replace")
    raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable")

# 通过 default 参数调用
output = json.dumps(payload, default=simple_default_handler)

两者在核心实现逻辑上高度相似,但在工程架构上有明确的职责划分:

  • default 回调函数:轻量且即用即走。适合临时脚本、一次性调试或者逻辑极其单一的单个函数内部。
  • 继承 JSONEncoder 类:高内聚、易于分层复用。通过类封装,可以方便地添加初始化参数(例如是否开启排序、是否强制 Base64 编码)、集成多类型扩展(如同时处理 datetime.date、decimal.Decimal、UUID),并且能够无缝对接基于类的 Web 框架(如定制 Flask 或 Django 的响应编码器)。

五、序列化闭环与核心注意事项

在实际项目中运用自定义 JSONEncoder 时,需注意以下关键细节以避免隐藏的逻辑隐患:

避坑提示:

1. 反序列化(Deserialization)非对称性:JSON 格式中只有数组和字符串。经由此编码器转换后,set 变成了 list,bytes 变成了 str。调用 json.loads() 时得到的数据结构将是列表与字符串,原对象的集合与二进制类型特征已丢失。若业务需要精确还原原生对象,须在反序列化阶段使用 object_hook 或显式做类型重构。

2. 元素级递归支持:JSONEncoder.default() 只在遇到无法处理的顶层或嵌套节点时才会被触发。如果一个集合内嵌套了其他未知类型,每次遇到未知类型该方法都会被再次调用,因此不必在集合分支中手动对列表项递归编码。

3. 保持 super().default(o) 兜底:千万不要在处理完 set 和 bytes 后返回 None 或静默忽略其他类型。若返回 None,所有无法识别的对象(如未处理的函数对象、自定义业务类)都会被错误序列化为 null,导致数据悄然失真而无法在源头捕获错误。

Python通过自定义JSONEncoder处理set或bytes类型序列化
图 4:自定义 JSON 序列化闭环与生产环境排查清单
相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表