DEBUG.WIKI
V.PS:让项目靠近用户,亚太、欧洲、北美 KVM VPS
IPPeak:从目标地区访问网页,住宅代理、SEO 监测、广告验证
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国

Pydantic 2.7 的 serialize_as_any=True 误报 circular reference

递归字段以 None 结束,运行时 duck typing 却报循环引用?核对 Pydantic 2.7 历史误报、包含修复的依赖组合和子类输出契约。

模型的递归字段已经是 None,model_dump() 正常,加上 serialize_as_any=True 却报 Circular reference detected (id repeated)。在 Pydantic 2.7 的特定旧版本路径中,这可能是递归类型处理的误报,不能看到循环引用错误就删掉字段。

先区分递归类型与实际对象环,再核对 Pydantic 和 pydantic-core 的版本组合。模型能打印出来,也不能单独证明真实数据没有环。

用终止于 None 的模型缩小范围

以下示例使用 Python 3.10 及以上,用于隔离类型和序列化参数;在开发环境运行,不放进生产请求路径:

from __future__ import annotations
import pydantic
import pydantic_core
from pydantic import BaseModel


class Node(BaseModel):
    next: Node | None = None


value = Node()
print(pydantic.__version__, pydantic_core.__version__)
print(value.model_dump())
print(value.model_dump(serialize_as_any=True))

Node 的类型定义引用自身,但这里的实例以 None 结束,并没有让 value.next 指回自己。旧 Pydantic 2.7.0 至 2.7.4 的这一参数路径曾出现误报;其中明确记录的环境是 2.7.3 与 core 2.18.4。这个限定不能扩展成所有 v2 都有问题。

分别保留普通输出、运行时参数输出和 JSON 输出的结果,记录是否存在自定义 serializer。实际对象指向自己、关联对象相互引用,仍是需要独立处理的真实循环,不属于上述最小模型。

使用包含修复的完整依赖组合

递归类型与运行时 duck typing 的相关处理已有修复。可核对的正式组合包括 Pydantic 2.12.0 与其要求的 pydantic-core 2.41.1;该 core 版本的递归模型测试已覆盖 serialize_as_any=True 的正常输出。这是已确认包含修复的版本组合,不表示它是最早修复版本,也不是要求较新的环境退回 2.12.0。

旧环境应先保存依赖锁文件、模型及预期输出,在独立环境选择项目兼容且包含修复的正式版本。让依赖管理器安装匹配的 core,不能只单独替换 core 来猜测兼容性。升级涉及验证和序列化行为,应核对调用方契约后再发布。

model_rebuild() 用于模型定义等问题,不应把它当作这条历史序列化回归的通用修复。也不要用 fallback=str 或删除递归字段把错误压成字符串输出,那可能改变数据结构并藏住实际缺陷。

关闭参数前,检查子类字段是否必须输出

按声明 schema 输出时,基类字段通常不会把子类新增字段全部带出去;duck typing 则可按实际对象输出这些字段。关闭 serialize_as_any 会改变输出范围,不能只检查“有没有报错”。

若接口本来只需要声明字段,可以在确认契约后使用默认输出。若需要某些字段按实际类型序列化,可评估字段级 SerializeAsAny,但应限制到必要范围,并检查嵌套字段。全局参数和字段注解不应未经核对就互换。

子类可能包含令牌、密码或内部状态。修复后既要看预期字段是否恢复,也要确认不该输出的字段仍被排除,不能把“输出更多”当作成功标准。

最后用无环的递归类型、真实环输入、子类新增字段及自定义 serializer 分别检查,保留预期的字典和 JSON 结构。新版本仍报错时,应依据新的最小模型和实际版本继续定位,不能沿用 2.7 的根因结论。需要回退依赖时同步恢复锁文件和相关字段配置,再检查已经输出的数据是否影响下游。