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 是正常输入语义,缺失字段是否允许则由模型声明决定;不能为了绕过这次回归,让这两类输入悄悄落到合法业务状态。


