使用显式Python合约在JSON中序列化datetime和Decimal
使用ISO时间戳字符串和十进制字符串,然后显式重建字段。一个完整的标准库示例显示了时区检查、精度和可预测的失败。
本文内容
简明答案
使用自定义json.dumps默认函数将带时区的datetime编码为ISO字符串,将有限的Decimal值编码为十进制字符串。在json.loads之后显式恢复命名字段。这保留了样本时间戳偏移量和Decimal('12.30')的表示,而不会将任意字符串视为类型对象。示例验证了两个字段,而不是完整的业务模式。
定义交换合约
JSON没有专用的datetime或Decimal类型。在选择编码器之前,定义每个额外的Python类型如何跨越边界。本指南使用特定的合约:时间戳是包含可用UTC偏移量的ISO字符串,金额是有限的十进制字符串。消费者显式重建两个字段。看起来像日期的字符串或数字的字符串不会自动成为类型化的Python对象。
示例使用Python标准库,并针对Python 3.11或更高版本。其构造的输入是2026年9月29日12:00的UTC datetime和Decimal('12.30')。目的在于保留此表示,而不仅仅是序列化任意对象或定义完整的支付模式。
序列化带时区的日期
datetime只有在tzinfo不是None且utcoffset()也返回值时才是感知的。编码器在调用isoformat()之前会检查这两个条件。拒绝无时区值可以避免静默分配机器的本地时区或将本地时钟读数与特定瞬间混淆。
示例UTC时间戳变为2026-09-29T12:00:00+00:00。一个偏移量字符串标识了该时刻的偏移量,但不会保留IANA区域名称或其未来的夏令时规则。仅在与您的合约匹配时才标准化为UTC;本示例不会将无时区时间戳重新解释为UTC。
保留Decimal为字符串
Decimal('12.30')保留了其十进制位数和尾随零。在序列化之前将其转换为二进制浮点数会更改表示并可能引入四舍五入。编码器返回str(obj),使金额成为JSON字符串。接收者可以在不先通过浮点数的情况下用Decimal重建它。
示例拒绝非有限的Decimal值。十进制字符串仍然没有指定货币、最大金额或允许的小数位数。这些是单独的业务约束。JSON也支持原生数值;在这里选择十进制字符串是一个显式的互操作性合约,而不是声称JSON不能包含数字。
构建显式编码器
完整的示例包括导入、编码、重建和示例输入。json.dumps仅在其常规编码器无法处理的对象上调用default。自定义函数处理datetime和Decimal;其他不支持的自定义对象会引发TypeError。普通字符串、整数、列表和字典仍然是内置编码器的责任。
allow_nan=False在序列化期间拒绝原生浮点NaN和Infinity。有限的Decimal检查是单独的,因为Decimal由自定义函数处理。不需要第三方依赖。下面的示例是示意性的;其预期输出是解释的,而不是作为执行的测试呈现的。
import json
from datetime import datetime, timezone
from decimal import Decimal, InvalidOperation
def contract_encoder(obj):
if isinstance(obj, datetime):
if obj.tzinfo is None or obj.utcoffset() is None:
raise TypeError("Timezone-aware datetime required")
return obj.isoformat()
if isinstance(obj, Decimal):
if not obj.is_finite():
raise TypeError("Finite Decimal required")
return str(obj)
raise TypeError("Unsupported custom value")
def reconstruct_data(data):
if not isinstance(data, dict):
raise ValueError("Object required")
if not all(isinstance(data.get(key), str) for key in ("timestamp", "amount")):
raise ValueError("timestamp and amount must be strings")
timestamp = datetime.fromisoformat(data["timestamp"])
if timestamp.tzinfo is None or timestamp.utcoffset() is None:
raise ValueError("Timezone-aware timestamp required")
amount = Decimal(data["amount"])
if not amount.is_finite():
raise ValueError("Finite amount required")
return {"timestamp": timestamp, "amount": amount}
data = {"timestamp": datetime(2026, 9, 29, 12, 0, tzinfo=timezone.utc),
"amount": Decimal("12.30")}
encoded = json.dumps(data, default=contract_encoder, allow_nan=False)
print(encoded)
restored = reconstruct_data(json.loads(encoded))
print(restored["timestamp"].isoformat(), str(restored["amount"]))
读取字段
json.loads恢复字典、列表和基本值。它将时间戳和金额留为字符串。reconstruct_data检查顶层值是否为对象,并且两个字段都是字符串,然后调用datetime.fromisoformat和Decimal显式。它还需要一个可用的时间戳偏移量和一个有限的金额。
不要将每个字符串转换为看起来像日期或数字。字段的含义来自协商的模式。这些检查涵盖了两个字段;它们不会拒绝每个额外属性或强制执行应用程序大小、货币、授权和范围策略。应用程序应添加其边界实际需要的约束。
检查说明性输出
对于构造的输入,第一行打印的是{ 'timestamp': '2026-09-29T12:00:00+00:00', 'amount': '12.30' }。第二行是2026-09-29T12:00:00+00:00 12.30。这些是从代码中得出的说明性预期行,而不是执行测试的报告。金额在JSON中被引号,并且在重建后仍然以零结尾。
将类型化的重建值与预期的合约进行比较。解码的字符串本身并不能证明重建已经发生。将输出合约更改为标记对象或JSON数值金额将需要消费者中的相应更改;不要意外混合格式。
可预测地处理无效值
编码无时区时间戳、非有限Decimal或另一个不支持的自定义对象会引发示例中的显式TypeError。格式不正确的JSON可能会引发json.JSONDecodeError。无效的ISO时间戳文本会引发ValueError。无效的十进制文本通常会在默认的十进制上下文下引发decimal.InvalidOperation,而不是ValueError。
重建检查会为错误的顶层类型、缺失或非字符串字段、无偏移量的时间戳和非有限金额引发ValueError。Decimal陷阱是可配置的,因此异常行为可能取决于上下文。在应用程序边界处理相关的失败,而不承诺通用的堆栈跟踪词汇或记录敏感有效负载。
了解JSON无法保留的内容
JSON不会记住数组是否源自元组或列表,或者字符串是否源自datetime或Decimal。因此,显式重建是此合约的一部分。从default返回字符串在这里是足够的;自定义JSONEncoder和JSONDecoder子类是可选的替代方案,而不是强制要求。
object_hook在解码后的JSON对象上被调用,而不是在每个原始字符串上。它可以支持显式设计的标记对象格式,但这将是与本示例中使用的两个命名字符串字段不同的合约。保留表示是有用的,但它并没有建立业务有效性、命名时区规则或安全资源消耗。
检查清单
- 使用Python 3.11或更高版本进行此示例。
- 在接受datetime之前检查tzinfo和utcoffset()。
- 将Decimal金额保留为JSON字符串,而不是将其转换为浮点数。
- 在重建期间要求具有字符串时间戳和金额字段的对象。
- 拒绝无限金额和无偏移量时间戳。
- 在输入边界处区分ValueError、json.JSONDecodeError和decimal.InvalidOperation。
适用范围
这是一个构造的标准库说明,而不是一个执行的测试。偏移量字符串不会保留命名时区规则。十进制字符串保留表示,但不保留货币或业务有效性。示例验证了两个字段和有限性;应用程序仍然需要范围、大小和授权策略。