DEBUG.WIKI
丽萨主机;为跨境业务选好 IP;原生 / 住宅 IP · 按套餐选择;查看 IP →
PanstarCloud:部署应用,接入 AI,云服务器与 AI 网关
IPPeak:从目标地区访问网页,住宅代理、SEO 监测、广告验证

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 文本;同时检查未知键、混合键和严格模式的实际接受范围,保留模型原有的业务限定。