Pydantic 跨模块泛型能验证,pickle 却找不到模型类
2.14.x 的动态参数化类可能登记在调用模块,却保留定义模块的身份。给持久化类型建立具名具体子类,并核对两端可导入路径。
泛型模型放在 models.py,另一个模块写 Response[Page[Item]]。对象创建和验证都正常,交给 pickle 时却报 attribute lookup Response[Page[Item]] on models failed。这时应检查具体模型的可导入身份,而不是先改字段类型。
Pydantic 2.14.0、2.14.1 的跨模块参数化有这条回归,2.13.5 的相同形状可以正常往返。动态生成的参数化类仍把 __module__ 记为泛型定义模块,却可能登记到执行参数化的调用模块;pickle 按模块与限定名称找回类时,两处便对不上。
给需要持久化的类型一个稳定名称
模型类型固定时,可在模块顶层定义具名具体子类,而不是只给动态参数化类赋一个变量别名。假设已有 models.py 定义了 Page 与 Response,完整结构如下:
# models.py
from typing import Generic, TypeVar
from pydantic import BaseModel
T = TypeVar("T")
class Page(BaseModel, Generic[T]):
items: list[T]
class Response(BaseModel, Generic[T]):
result: T
在负责具体类型的模块中定义真正的类:
# app_models.py
from pydantic import BaseModel
from models import Page, Response
class Item(BaseModel):
name: str
class ItemResponse(Response[Page[Item]]):
pass
后续创建 ItemResponse 实例。它有 app_models.ItemResponse 这样的稳定类名;读取方必须安装相容代码并能正常导入该模块,不能把类定义藏进函数或临时 Notebook 单元格。只写 ItemResponse = Response[...] 仍是原动态类,不会自动建立新的类身份。
传输数据也可以与传输 Python 类分开
如果协议只需要字段内容,可以明确发送模型的 JSON 数据,由接收方用约定的具体模型重新验证。这会改变传输格式与异常处理,需要发送、接收两端一起调整,不能把 JSON 文本放进原 pickle 通道却保持旧读取器。
这里与ValidationError 的 sequence_str 反序列化失败不同:本题在查找模型类时失败,后者是异常错误类型重建。也不要为此批量改写 sys.modules 或动态类的 __module__,那可能影响同一泛型的其他缓存与调用者。
保留依赖锁文件、模块路径与既有消息格式。复查要包含真实发送进程与读取进程,检查恢复后的类、嵌套 Item 和字段约束,不只看 pickle.dumps() 是否成功。pickle 只用于原来可信的数据通道,不为这次修复新增不可信对象的加载入口。
具名子类不会追溯修复旧消息保存的动态类名。切换前明确旧队列或存档如何读取;需要回退时同步恢复两端模型代码与格式,避免新旧消费者各自寻找不同类型。


