Pydantic 导出的整数枚举字典,为什么 JSON 读不回来
JSON 对象键只能是字符串,普通整数值 Enum 的验证不一定接受数字字符串。为既定协议显式映射合法键,或改为键值记录结构,保留未知键拒绝与碰撞检查。
Report(counts={Status.OK: 3}) 能建立模型,model_dump_json() 也能输出,紧接着 model_validate_json() 却报枚举键不合法。常见条件是 Status 继承普通 Enum,成员值为整数,例如 OK = 200。
JSON 对象键只能是字符串。模型输出的键变成 "200",读回时普通整数值 Enum 并不必然把这个字符串解释成整数 200。它与 IntEnum、字符串枚举以及普通 dict[int, ...] 的验证路径不同,不能根据其中一种能往返就推定全部等价。
已有协议可在字段入口明确映射
如果现有协议已经约定使用这些数字字符串,可只接受已列出的成员值,而不是对任何键执行 int():
from enum import Enum
from pydantic import BaseModel, field_validator
class Status(Enum):
OK = 200
NOT_FOUND = 404
class Report(BaseModel):
counts: dict[Status, int]
@field_validator("counts", mode="before")
@classmethod
def decode_keys(cls, value):
if not isinstance(value, dict):
return value
allowed = {str(member.value): member for member in Status}
converted = {}
for key, count in value.items():
if isinstance(key, Status):
member = key
elif isinstance(key, str) and key in allowed:
member = allowed[key]
else:
raise ValueError("未知状态键")
if member in converted:
raise ValueError("状态键重复")
converted[member] = count
return converted
这是一个有意收窄的接口:Python 模式接受现成 Status,JSON 模式接受规定的字符串键;未知键仍然失败。值本身继续由原来的 int 字段规则验证。若原来还允许裸整数键,应明确是否保留,再补对应分支,不能让修复隐含改变接口。
碰撞检查也不能省略。Python 字典可能同时有一个枚举键和它对应的字符串键,映射后指向同一个状态;直接覆盖会悄悄丢掉其中一个计数。JSON 文本自身的重复键是更早的解析问题,字段验证拿到普通字典后,不能承诺找回已经被解析器覆盖的输入。
改成 IntEnum 或键值列表都涉及契约变化
若枚举本来就需要整数子类语义,可以评估 IntEnum,但它的比较、类型关系和调用方行为与普通 Enum 不完全相同,不能只为序列化更换基类。另一种结构是把映射写成状态与计数的记录列表,让枚举值处在 JSON 的普通值位置;这样仍要定义重复状态怎么处理。
正式 core 2.50.1 的普通 Enum 与 IntEnum 仍有不同验证分支。核对往返时应比较状态成员与计数,不只比较 JSON 文本;同时检查未知键、混合键和严格模式的实际接受范围,保留模型原有的业务限定。


