Python JSON序列化深度解析:从基础映射到自定义类型处理

📅 2026/8/6 4:46:43
Python JSON序列化深度解析:从基础映射到自定义类型处理
1. 从一次数据交换的“翻车”说起最近在帮一个做电商数据分析的朋友排查一个线上问题他们有一个定时任务负责将每日的商品销售数据聚合后通过HTTP API推送给下游的BI系统。某天早上BI系统报警说数据格式异常无法解析。我拿到日志一看发现他们推送的JSON字符串里有一个字段的值是NaN。这个NaN在Python的数值计算里很常见表示“不是一个数字”但问题在于标准的JSON规范里根本没有NaN这个值。下游系统用的是Java它的JSON解析器看到这个不认识的玩意儿直接就抛异常了。这个看似简单的问题恰恰点出了Python对象转JSON字符串这个操作里最核心、也最容易踩坑的地方序列化Serialization。我们每天都在用json.dumps()觉得它像喝水一样自然但你真的了解它背后把Python的“方言”翻译成JSON“世界语”的完整规则吗你知道哪些Python对象能直接转哪些会“水土不服”吗当遇到像NaN、datetime对象或者自定义类这种“刺头”时你又该怎么优雅地处理这篇文章我就以一个踩过无数坑的老兵身份带你彻底拆解Python中的JSON序列化。我们不止看json.dumps()这个函数怎么用更要深挖它的默认行为、扩展机制和那些藏在细节里的魔鬼。无论你是刚接触数据交换的新手还是想优化现有序列化逻辑的老鸟相信都能找到对你有用的东西。2. JSON序列化的核心json.dumps()的默认行为探秘json.dumps()是json模块的绝对主力它的任务就是把一个Python对象“倾倒”dump成一个JSON格式的字符串。我们先从它最基础、最常用的形态开始理解。2.1 基础数据类型的“直通车”对于Python内置的基础数据类型json.dumps()的映射规则非常直观几乎可以认为是“无损”转换除了个别特例我们后面会讲。import json # 基础类型转换示例 data { string: Hello, JSON, integer: 42, float: 3.14159, boolean_true: True, boolean_false: False, none_value: None, list: [1, 2, 3], nested_dict: {key: value} } json_str json.dumps(data, indent2) # 使用indent让输出更美观 print(json_str)输出会是标准且漂亮的JSON{ string: Hello, JSON, integer: 42, float: 3.14159, boolean_true: true, boolean_false: false, none_value: null, list: [1, 2, 3], nested_dict: { key: value } }这里有几个关键映射关系你必须了然于胸str-string: 直接对应。int,float(除了特殊值) -number: 直接对应。bool(True/False) -boolean(true/false): 注意Python是大写开头JSON是小写。None-null: 这是Python里的空对应JSON里的空。list,tuple-array: 无论是列表还是元组都会变成JSON数组。注意元组在序列化后其“元组”的特性就丢失了反序列化回来会变成列表。dict-object: Python字典完美对应JSON对象。注意JSON对象的键必须是字符串。Python字典的键可以是整数等但在序列化时json.dumps()会调用str()函数将它们转换为字符串。例如{1: “one”}会变成{“1”: “one”}。如果你的业务逻辑依赖键的类型这里就是个潜在的坑。2.2 那些“水土不服”的Python类型默认的禁区现在来说说开头那个NaN的问题。Python的float类型里有几个特殊成员nan(非数字),inf(正无穷大),-inf(负无穷大)。它们在科学计算中很有用但在JSON的世界里没有它们的合法身份。import math import json problem_data {value: math.nan, infinite: math.inf} try: json.dumps(problem_data) except TypeError as e: print(f出错了{e}) # 输出出错了Out of range float values are not JSON compliant默认情况下json.dumps()会直接抛出一个TypeError告诉你这些浮点数值超出了JSON的合规范围。这其实是json模块的一种保护机制防止你生成一个下游解析器可能无法处理的、非标准的JSON字符串。除了特殊浮点数还有哪些常见的“禁区”呢datetime.date和datetime.datetime对象JSON没有原生的日期时间类型。from datetime import datetime json.dumps({now: datetime.now()}) # 会抛出 TypeError自定义的类实例你定义的User、Product类json模块并不知道如何把它们变成字典或列表。class Product: def __init__(self, id, name): self.id id self.name name p Product(1, Python Book) json.dumps(p) # 会抛出 TypeError: Object of type Product is not JSON serializableset集合JSON没有集合类型。json.dumps({1, 2, 3}) # 会抛出 TypeError包含不可序列化对象的复杂结构比如一个字典它的某个值是一个datetime对象。当遇到这些情况时我们不能指望json.dumps()的默认行为必须进行“人工干预”这就是自定义序列化的用武之地。3. 进阶武器default参数与自定义序列化函数json.dumps()之所以强大是因为它提供了扩展点。当它遇到无法处理的类型时会回头问我们“这个家伙该怎么处理” 这个回头问的机制就是default参数。3.1default参数的工作机制default参数接受一个函数。当json.dumps()遇到无法序列化的对象obj时它会调用这个函数并把这个对象传进去。我们的任务就是在这个函数里把这个对象转换成json.dumps()认识的东西通常是字典、列表、字符串、数字等。如果default函数也处理不了这个对象它应该抛出TypeError这样json.dumps()就会把错误继续往上抛。让我们用default来解决datetime和自定义类的问题。from datetime import datetime, date import json def custom_serializer(obj): 自定义序列化函数处理多种特殊类型。 # 处理 datetime 对象 if isinstance(obj, datetime): # 转换为 ISO 8601 格式的字符串这是跨语言交换日期时间的通用标准 return obj.isoformat() # 处理 date 对象 elif isinstance(obj, date): return obj.isoformat() # 处理自定义的 Product 类实例 elif isinstance(obj, Product): # 将其转换为字典。这里我们选择暴露哪些属性。 return {product_id: obj.id, product_name: obj.name} # 处理 set 集合 elif isinstance(obj, set): # 转换为列表 return list(obj) # 对于其他无法处理的类型主动抛出 TypeError else: raise TypeError(fObject of type {obj.__class__.__name__} is not JSON serializable) # 准备包含多种类型的数据 complex_data { timestamp: datetime.now(), today: date.today(), product: Product(101, Advanced Python), tags: {python, json, serialization}, normal_field: This is fine } # 使用自定义序列化器 json_str json.dumps(complex_data, defaultcustom_serializer, indent2) print(json_str)输出结果类似于{ timestamp: 2023-10-27T14:30:15.123456, today: 2023-10-27, product: { product_id: 101, product_name: Advanced Python }, tags: [ json, python, serialization ], normal_field: This is fine }看所有“刺头”都被我们优雅地“招安”了变成了标准JSON的一部分。default函数就像一个万能翻译官把Python特有的“方言”翻译成JSON能懂的“普通话”。3.2 处理特殊浮点数allow_nan参数与自定义策略回到开头的NaN问题。除了用default函数json.dumps()本身也提供了一个参数allow_nan。这个参数控制是否允许序列化nan,inf,-inf。allow_nanTrue(默认值): 允许序列化但会使用JavaScript中的NaN,Infinity,-Infinity来表示。注意这会产生非标准的JSON很多严格的JSON解析器如Java的Jackson默认配置会拒绝解析。import math data {value: math.nan} print(json.dumps(data, allow_nanTrue)) # 输出: {value: NaN}allow_nanFalse: 禁止序列化遇到这些值直接抛出TypeError。这是最安全、最符合标准的方式。在实际生产环境中我强烈建议将allow_nanFalse作为默认选项从源头杜绝非标准JSON的产生。那么如果数据里真的出现了这些特殊值我们该怎么办答案是在序列化之前就处理好它们。更健壮的做法在数据清洗层处理与其依赖序列化时的补救不如在业务逻辑或数据准备阶段就清洗掉这些非法值。我们可以定义一个数据清洗函数def sanitize_floats(obj): 递归遍历数据结构将 nan/inf/-inf 替换为 None 或其它安全值。 这是一个深度清洗函数。 if isinstance(obj, float): if math.isnan(obj): return None # 或者 return 0.0根据业务逻辑决定 elif math.isinf(obj): return None # 或者 return一个很大的数如 1e10 else: return obj elif isinstance(obj, dict): return {k: sanitize_floats(v) for k, v in obj.items()} elif isinstance(obj, (list, tuple)): return [sanitize_floats(item) for item in obj] else: return obj # 使用清洗后的数据进行序列化 dirty_data {a: 1.0, b: math.nan, c: [math.inf, 2.0]} clean_data sanitize_floats(dirty_data) safe_json json.dumps(clean_data, allow_nanFalse) # 此时可以安全地设置 allow_nanFalse print(safe_json) # 输出: {a: 1.0, b: null, c: [null, 2.0]}这种做法将数据合规性的责任前置使得序列化过程变得纯粹和可靠是工程上更推荐的做法。4. 性能与可读性的权衡json.dumps()的其他关键参数json.dumps()不止有default和allow_nan还有其他几个参数深刻影响着输出结果和性能。4.1indent美观与体积的博弈indent参数用于美化输出添加缩进和换行。这在开发调试、生成给人看的配置文件时非常有用。indentNone(默认): 输出紧凑的、没有多余空格的JSON体积最小适合网络传输。indent一个整数: 指定缩进的空格数。indent2或indent4是最常见的选择。重要影响添加缩进会显著增加字符串的体积有时能增加50%以上。对于需要高频传输的大数据量场景务必使用indentNone。data {name: Alice, age: 30, city: New York} compact json.dumps(data) # {name: Alice, age: 30, city: New York} pretty json.dumps(data, indent2) # 输出 # { # name: Alice, # age: 30, # city: New York # } print(f紧凑版长度{len(compact)}) print(f美化版长度{len(pretty)})4.2separators微调输出格式这个参数可以改变JSON中各项之间的分隔符。它是一个二元组(item_separator, key_separator)。默认值是(, , : )即在逗号后有一个空格在冒号后有一个空格。你可以将其改为(,, :)来生成最紧凑的JSON连空格都去掉在极端追求体积时使用。注意修改这个参数可能会破坏一些对格式有严格要求的下游系统虽然很少见。data {x: 1, y: 2} print(json.dumps(data, separators(,, :))) # 输出: {x:1,y:2}4.3sort_keys确保输出顺序稳定Python字典从3.7版本开始保证了插入顺序但更早的版本或某些特定情况下为了确保生成的JSON字符串完全一致例如用于生成数字签名或缓存键可以使用sort_keysTrue。这会让字典的键按照字母顺序排序后输出。data {z: 3, a: 1, m: 2} print(json.dumps(data, sort_keysTrue, indent2)) # 输出 # { # a: 1, # m: 2, # z: 3 # }4.4ensure_ascii处理非ASCII字符默认情况下 (ensure_asciiTrue)json.dumps()会将所有非ASCII字符如中文转义为\uXXXX的Unicode序列。这保证了生成的JSON字符串是纯ASCII的兼容性最好但可读性差。data {name: 张三} print(json.dumps(data)) # 输出: {name: \u5f20\u4e09} print(json.dumps(data, ensure_asciiFalse)) # 输出: {name: 张三}如果你的上下游系统都明确支持UTF-8编码现在绝大多数都是那么设置ensure_asciiFalse会让数据更直观。但如果你在和一个非常古老或要求严格ASCII的系统通信就需要保持默认的True。5. 实战中的序列化策略与模式了解了所有工具之后我们需要在实战中把它们组合起来形成稳定可靠的序列化策略。这里分享几种我常用的模式。5.1 为特定类定义__json__()方法与其在default函数里写一堆isinstance判断不如让类自己告诉外界该如何序列化自己。我们可以定义一个约定如果一个对象有__json__()方法json.dumps()就调用它来获取可序列化的表示。我们需要一个能识别这个约定的default函数def default_with_json_method(obj): 优先尝试调用对象的 __json__ 方法 如果没有再尝试其他通用序列化逻辑。 # 首先检查是否有 __json__ 方法 if hasattr(obj, __json__) and callable(obj.__json__): return obj.__json__() # 然后处理一些通用类型 elif isinstance(obj, (datetime, date)): return obj.isoformat() elif isinstance(obj, set): return list(obj) # ... 其他处理 else: raise TypeError(fObject of type {obj.__class__.__name__} is not JSON serializable) # 在自定义类中实现 __json__ 方法 class User: def __init__(self, user_id, username, email): self.user_id user_id self.username username self.email email self._password_hash hashed_secret # 敏感信息不应序列化 def __json__(self): # 明确控制哪些属性可以暴露给JSON return { id: self.user_id, username: self.username, email: self.email # 注意不包含 _password_hash } user User(1, alice, aliceexample.com) data {user: user, action: login} json_str json.dumps(data, defaultdefault_with_json_method, indent2) print(json_str)这种模式将序列化逻辑封装在类内部更符合面向对象的设计原则也更容易维护。5.2 使用json.JSONEncoder子类进行全局定制如果你的项目中有大量需要自定义序列化的类型或者你想在整个项目中应用统一的序列化规则那么继承json.JSONEncoder并重写它的default()方法是一个更优雅、更强大的选择。json.JSONEncoder是json.dumps()背后真正的执行者。我们可以创建一个自己的编码器import json from datetime import datetime, date from decimal import Decimal import uuid class CustomJSONEncoder(json.JSONEncoder): 自定义JSON编码器统一处理项目中的特殊类型。 def default(self, obj): # 处理日期时间 if isinstance(obj, (datetime, date)): return obj.isoformat() # 处理Decimal金融计算常用JSON没有对应类型 elif isinstance(obj, Decimal): # 通常转换为字符串以避免浮点数精度问题也可以转换为float return str(obj) # 处理UUID elif isinstance(obj, uuid.UUID): return str(obj) # 处理有 __json__ 方法的对象 elif hasattr(obj, __json__) and callable(obj.__json__): return obj.__json__() # 处理numpy数组如果项目中使用numpy # elif isinstance(obj, np.ndarray): # return obj.tolist() # 最后调用父类方法它会抛出TypeError return super().default(obj) # 使用自定义编码器 # 方法1传递给 json.dumps 的 cls 参数 data { id: uuid.uuid4(), price: Decimal(99.99), created_at: datetime.now() } json_str json.dumps(data, clsCustomJSONEncoder, indent2) print(json_str) # 方法2直接实例化编码器使用更灵活可以复用实例 encoder CustomJSONEncoder(indent2) json_str encoder.encode(data) print(json_str)使用JSONEncoder子类的好处是你可以将这个编码器实例作为一个单例在整个应用中共享比如配置到你的Web框架如Flask的json_encoder或RPC客户端中实现一劳永逸的序列化配置。5.3 性能考量default函数与JSONEncoder的对比在性能敏感的场景下例如每秒需要序列化成千上万个对象序列化的开销不容忽视。default函数对于每个无法序列化的对象都会调用一次这个函数。如果这个函数内部有大量的isinstance判断并且需要处理多种类型那么每次调用都会走一遍这个判断链开销会累积。JSONEncoder子类其default()方法同样面临isinstance判断链的问题。但从设计模式上看它更清晰且可以通过将编码器实例化一次来避免一些重复开销。优化建议减少类型判断次数在default方法中将最频繁出现的类型判断放在最前面。使用字典映射对于已知的、有限的类型集合可以使用一个{类型: 处理函数}的映射来代替一长串if-elif通过obj.__class__来查找处理函数这在类型很多时可能更快。避免过度序列化只序列化真正需要传输的数据。在将对象转换成字典时仔细挑选字段不要图省事直接obj.__dict__这可能会暴露内部状态或敏感信息。对于极其严苛的性能场景可以考虑使用更快的序列化库如ujson(UltraJSON) 或orjson。这些库用C实现速度远超标准库的json模块但需要注意它们的API可能略有不同对自定义类型的支持方式也可能不一样。6. 反序列化解码的对应考量json.loads()与object_hook序列化是把Python对象变成字符串反序列化则是把字符串变回Python对象。有“送出去”的策略就得有“接回来”的方案。json.loads()是json.dumps()的逆过程它也有一个强大的扩展点object_hook。6.1object_hook的基本用法object_hook是一个函数它在json.loads()解析完一个JSON对象即字典后会把这个字典传给它。我们可以在这个函数里决定是否要将这个字典转换成某种特定的Python对象。例如我们之前把Product对象序列化为{product_id: 101, product_name: Advanced Python}。现在我们想把它变回Product实例。def object_hook_for_product(dct): 检查字典是否具有特定特征如果是则转换为自定义对象。 # 方法1通过特定字段判断 if product_id in dct and product_name in dct: return Product(dct[product_id], dct[product_name]) # 方法2通过一个特殊的类型标记字段更通用 # if _type in dct and dct[_type] product: # return Product(dct[id], dct[name]) # 如果不是我们要处理的类型就原样返回字典 return dct # 假设这是接收到的JSON字符串 received_json {product: {product_id: 101, product_name: Advanced Python}, count: 5} # 使用 object_hook 进行反序列化 decoded_data json.loads(received_json, object_hookobject_hook_for_product) print(decoded_data) # 输出{product: __main__.Product object at 0x..., count: 5} print(decoded_data[product].name) # 输出Advanced Python注意object_hook是递归应用的。也就是说对于嵌套的字典每一层在解析后都会经过这个钩子函数。6.2 与序列化策略配对使用一个健壮的序列化/反序列化方案需要配对设计。如果你在default函数或JSONEncoder中为某种类型添加了特殊的序列化逻辑那么你也应该在object_hook或自定义的JSONDecoder中为其添加对应的反序列化逻辑这样才能实现对象的“往返”无损转换。一种常见的配对模式是使用“类型标记”type hint。在序列化时往生成的字典里插入一个特殊的字段如_type、__class__标明原始对象的类型。在反序列化时object_hook就根据这个标记来实例化对应的类。class CustomJSONEncoderWithType(json.JSONEncoder): def default(self, obj): if isinstance(obj, Product): # 添加类型标记 dct {product_id: obj.id, product_name: obj.name, _type: Product} return dct elif isinstance(obj, User): dct {user_id: obj.user_id, username: obj.username, _type: User} return dct # ... 处理其他类型 return super().default(obj) def custom_object_hook(dct): _type dct.get(_type) if _type Product: # 移除类型标记用剩余字段创建对象 dct.pop(_type) return Product(**dct) # 假设Product构造函数接受这些关键字参数 elif _type User: dct.pop(_type) return User(**dct) return dct # 序列化 encoder CustomJSONEncoderWithType() data {item: Product(1, Book), user: User(2, Bob)} json_str encoder.encode(data) # 反序列化 decoded_data json.loads(json_str, object_hookcustom_object_hook) print(decoded_data[item]) # 是一个Product对象 print(decoded_data[user]) # 是一个User对象这种模式在需要完全恢复对象图比如缓存、RPC的场景下非常有用但它也使得生成的JSON与你的Python代码耦合更紧不再是“纯数据”了。因此在纯粹的、跨语言的数据交换接口中应谨慎使用。7. 总结与最佳实践心得处理JSON序列化远不止调用一个json.dumps()那么简单。它涉及到数据合规性、系统兼容性、性能和安全。结合我多年的经验这里有一些总结性的建议明确数据边界首先想清楚你序列化的数据是要给谁用是另一个用Python写的服务还是一个用Java/C#/Go写的服务还是一个前端JavaScript应用这直接决定了你能在序列化过程中“夹带”多少私货比如类型标记。对于跨语言通信坚持使用最标准、最简单的JSON数据类型。拥抱ISO 8601处理时间对于日期时间obj.isoformat()生成的字符串如”2023-10-27T14:30:15.123456″是国际标准几乎所有语言的库都能正确解析。避免使用自定义的时间戳或格式除非有极强的历史原因。特殊浮点数的处理要前置将nan、inf的处理视为数据清洗的一部分而不是序列化的一部分。在数据进入业务逻辑或准备发送前就将其转换为None或合法的数值。始终使用allow_nanFalse以确保生成标准JSON。安全性第一永远不要直接序列化对象的__dict__。你可能会意外暴露密码哈希、内部状态、数据库连接等敏感信息。始终显式地定义一个字典只包含需要公开的字段。对于从外部接收的反序列化数据也要保持警惕避免通过object_hook实例化任意类这可能导致安全问题。性能与可读性的平衡在开发调试时使用indent让日志和输出更易读。在生产环境传输数据时务必使用indentNone和separators(‘,’, ‘:’)来最小化数据体积。ensure_ascii参数根据你的上下游系统编码支持情况来决定。建立项目级的序列化规范如果项目复杂尽早确定是使用分散的default函数还是使用一个统一的CustomJSONEncoder。我个人的倾向是对于中型以上项目定义一个项目级的JSONEncoder子类并在框架配置中全局指定这样最利于维护和保持一致性。不要忽视反序列化设计序列化方案时一定要同时考虑反序列化。思考你添加的额外信息如类型标记在反序列化时是否必要以及如何安全、正确地还原。json.loads()的object_hook是你的好帮手。Python的json模块提供的工具就像一套精巧的瑞士军刀default、cls(JSONEncoder)、object_hook这些参数是上面的各种小工具。理解每件工具的用途和局限根据不同的场景组合使用你就能游刃有余地应对各种数据交换的挑战避免像我朋友那样在凌晨被一个NaN引发的报警吵醒。