DEBUG.WIKI
腾讯云;让你的想法跑起来;轻量服务器 · 建站 / Docker / API;查看产品 →
V.PS:让项目靠近用户,亚太、欧洲、北美 KVM VPS
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →

Pydantic 解析 JSON 时 Enum._missing_ 收到 None,字典验证却正常

枚举自定义 _missing_ 对字典输入正常,JSON 输入却收到 None,核对旧验证路径,使用匹配修复版本或明确的字段前置转换。

同一份枚举字段,model_validate({"state": "READY"}) 能按 _missing_() 做大小写兼容,改成 model_validate_json() 却失败;在钩子里检查,收到的值居然是 None。如果 JSON 原文确实是字符串,应核对 Pydantic 的枚举 JSON 验证路径,而不是先给业务数据补一个默认状态。

明确出现过的组合包括 Pydantic 2.12.5/core 2.41.5,以及后续 2.13.4、2.13.5。可核对的 Pydantic 2.14.1/core 2.50.1 源码已在枚举回退前把输入转换成 Python 对象,再调用枚举类;不要把旧组合的现象写成当前所有版本都存在的规则。

比较的是钩子收到的值,不只是最终枚举

Python 的 _missing_() 会在按值查找枚举成员失败时获得待查值。它应返回一个合法成员,或者返回 None 表示仍无法处理。

下面的枚举只允许对大小写做兼容,不把任意未知状态都吞掉:

from enum import Enum
from pydantic import BaseModel

class State(Enum):
    READY = "ready"
    DONE = "done"

    @classmethod
    def _missing_(cls, value):
        if isinstance(value, str):
            normalized = value.lower()
            for member in cls:
                if member.value == normalized:
                    return member
        return None

class Job(BaseModel):
    state: State

from_python = Job.model_validate({"state": "READY"})
from_json = Job.model_validate_json('{"state": "READY"}')

业务期望两条入口都得到 State.READY。在受影响路径中,JSON 的未知枚举字符串可能在回退调用时变成 None,导致合法兼容值被拒绝。如果 _missing_() 不检查类型就调用 .lower(),还可能直接产生属性访问错误。

有些实现对任何输入都返回同一个兜底成员,最终值看起来一样,却仍可能丢失原始输入。需要确认钩子收到的实际值,不能只以“模型验证成功”排除这个问题。

使用修复组合,或在字段入口显式转换

可以先在隔离环境选择包含修复且与项目兼容的正式 Pydantic 版本,让依赖管理器安装它要求的 core,再用上述两条入口比较。

暂时不能升级时,若该字段本来就允许按枚举类的规则处理字符串,可在 mode="before" 的字段验证器中显式构造枚举。下例沿用上面的 State:

from pydantic import BaseModel, field_validator

class Job(BaseModel):
    state: State

    @field_validator("state", mode="before")
    @classmethod
    def convert_state(cls, value):
        if isinstance(value, str):
            return State(value)
        return value

前置验证器看到解码后的字段值,State(value) 直接按真实字符串调用枚举逻辑,成功后返回枚举实例。未知字符串仍会由枚举构造抛出 ValueError,进而成为字段验证错误。若业务使用严格模式,这个前置转换会显式允许这里的字符串,必须确认这正是接口希望保留的规则。

不要把所有 model_validate_json() 机械替换成 json.loads() 加 model_validate()。JSON 模式和 Python 模式在严格校验、日期等类型上可能有不同接受规则,整条入口替换会扩大行为变化;只处理确认受影响的字段更容易核对。

修复后比较正常成员值、可兼容的大小写、完全未知字符串、JSON null 和真正缺失的字段。null 对应 None 是正常输入语义,缺失字段是否允许则由模型声明决定;不能为了绕过这次回归,让这两类输入悄悄落到合法业务状态。