Pydantic 校验分数字符串,却直接抛出 ZeroDivisionError
旧 Pydantic 的 Fraction 字符串验证可能把零分母直接抛成 ZeroDivisionError。区分字段解析与业务除法,并限定错误转换范围。
接口模型的字段类型是 fractions.Fraction,提交 "6/0" 后,外层原本只处理 ValidationError 的逻辑没有接住错误,调用栈直接出现 ZeroDivisionError: Fraction(6, 0)。在特定旧版 Pydantic 中,分数构造函数的异常没有完整转换成验证错误,非法输入因此可能进入服务的内部错误分支。
这条路径有 Pydantic 2.12.5、pydantic-core 2.41.5 的明确记录。可核对的正式组合 Pydantic 2.14.1 与 core 2.50.1 已在核心分数验证中处理零分母异常,并有相关验证测试;这只是一个确认包含修复的组合,不表示最早修复版本,也不要求更高版本回退。
零分母不是普通字符串格式错误
Python 标准库 Fraction("6/0") 会抛 ZeroDivisionError,而无法解析的文本通常抛 ValueError。旧验证器只处理了后一类异常,因此两种非法输入可能走向不同出口。
排查时区分业务自己调用 Fraction() 与 Pydantic 字段验证。下面是旧版相关验证入口的最小形状:
from fractions import Fraction
from pydantic import TypeAdapter
adapter = TypeAdapter(Fraction)
adapter.validate_strings("6/0")
如果实际堆栈指向自己编写的除法运算,即使同样出现 ZeroDivisionError,也不是这条分数解析路径。不能把所有除零都统一标成“参数不合法”而掩盖应用计算错误。
优先使用包含修复的匹配依赖
升级前核对实际环境中的 Pydantic 与 core 版本,按项目依赖策略更新整个匹配组合。不要只单独安装一个新的 pydantic-core,让 Python 包与核心版本脱离声明的依赖关系。
修复后的预期是:非法分数字符串在验证入口形成 ValidationError,调用方原有的字段错误处理能接住它;分母为零的输入仍然必须被拒绝,不能自动替换成零或无穷大。应用若有自定义异常映射,也应确认它没有将这次验证失败转换成成功响应。
暂时不能升级时,在字符串解析处转换异常
若受依赖约束无法立即升级,可以给这个字段加一个范围明确的前置验证器。以下示例适用于 Python 3.10 及以上;只提前解析字符串,其他类型仍交给后续字段验证:
from fractions import Fraction
from typing import Annotated
from pydantic import BaseModel, BeforeValidator, ValidationError
def parse_fraction_text(value):
if isinstance(value, str):
try:
return Fraction(value)
except (ValueError, ZeroDivisionError) as exc:
raise ValueError("invalid fraction text") from exc
return value
class Request(BaseModel):
ratio: Annotated[Fraction, BeforeValidator(parse_fraction_text)]
try:
Request.model_validate({"ratio": "6/0"})
except ValidationError as exc:
print(exc.errors()[0]["loc"])
Pydantic 会把前置验证器中的 ValueError 归入字段验证错误,错误位置应指向 ratio。这个方案改变了该字符串分支的错误信息,不能要求它与新版本的 fraction_parsing 错误码完全一致;若客户端依赖错误码,要同时核对接口契约。
不要在整个请求处理函数外捕获任意 Exception,也不要给所有除零都返回默认值。自定义解析器只解决明确的字符串输入路径,升级后能否删除它,要比较原有输入规则再决定。
验收时分别提交 "1/3"、"6/0"、无法解析的文本,以及接口允许的已有 Fraction 对象。正常分数应保留精确的有理数值,非法输入应落到预期字段错误;同时检查 JSON、Python 对象和实际接口入口,不能只测其中一条路径。


