cattrs 单个类型能转换,Union 却报 KeyError: kind
cattrs 26.2.1 的 Literal 联合判别按原字段名读取,忽略 kind 到 type 的 rename。保留成员转换 hook,显式为联合类型分派,检查输入标签与输出格式。
两个 attrs 类型都有 kind: Literal[...] 字段,传输格式却把它叫作 type。给每个成员注册 kind=override(rename="type") 后,单独转换 A 正常,转换 Union[A, B] 却报:
KeyError: 'kind'
cattrs 的成员转换与联合类型判别分两步。每个成员知道字段改名了,不代表负责选择成员的判别函数也使用了同一个外部名称。
联合判别发生在成员 hook 之前
cattrs 26.2.1 的默认判别器会优先寻找各成员共有的 Literal 字段。这个分支使用原属性名,随后按 data["kind"] 取标签;输入只有 type,于是还没进入 A 或 B 的转换函数就失败。
同一判别器的“唯一字段”分支会处理 rename,所以有些联合类型正常,并不说明 Literal 分支也正常。不能据此给所有输入额外补一个 kind 字段,让协议同时带两个可能矛盾的标签。
截至 2026 年 10 月 23 日,正式 26.2.1 仍有这条分支,仅安装这版不能消除该错误。使用更高版本时,也应核对实际运行版本及对应判别代码,再决定是否撤掉兼容处理。
固定协议可以显式选择成员
在不改变外部 type 字段的前提下,可以为这个具体 Union 注册结构转换 hook,按已认可的标签映射分派,再继续调用成员自己的校验。下面示例只定义 A、B 两种记录,保留成员字段的 Literal 验证:
from typing import Literal, Union
from attrs import define
from cattrs import Converter
from cattrs.gen import make_dict_structure_fn, override
@define
class A:
kind: Literal["a"]
a_val: int
@define
class B:
kind: Literal["b"]
b_val: int
c = Converter()
for cls in (A, B):
c.register_structure_hook(
cls,
make_dict_structure_fn(cls, c, kind=override(rename="type")),
)
Record = Union[A, B]
targets = {"a": A, "b": B}
def structure_record(data, _):
if not isinstance(data, dict):
raise ValueError("record must be a dict")
tag = data.get("type")
if not isinstance(tag, str) or tag not in targets:
raise ValueError("invalid record type")
return c.structure(data, targets[tag])
c.register_structure_hook(Record, structure_record)
这段入口只接受 dict,缺失或未知标签明确拒绝。项目若允许其他 Mapping 或有自己的错误结构,需要在这个边界按原契约实现,不要不加区分地放宽。也不要让标签直接决定任意导入路径或任意类名;这里只映射到程序明确允许的两个类型。
联合 hook 继续把原输入交给成员 hook,因此 a_val、b_val 的类型与必填规则仍由原来的转换处理。改为 c.structure(data, Record) 会再次回到自己,不能在分派函数内递归调用同一个 Union。
读写格式分别验收
注册的是 structure hook,只解决读取联合类型的分派。需要把对象写回外部协议时,仍要单独核对 unstructure 的 rename 与联合序列化规则,不能假定这段代码已经同时配置了输出。
用 {"type": "a", "a_val": 1} 与 B 的有效样本检查实际类型和字段值,再验证缺失标签、未知标签、错误成员字段和同时提供冲突标签的输入。保留项目原本的额外字段策略;若要求拒绝额外键,应在 converter 及生成成员 hook 时明确配置并重新验收。
后续采用包含修复的正式版本时,可对同一批样本比较默认判别与自定义 hook,再决定移除兼容代码。只看到 KeyError 消失,不足以证明成员选择和外部协议仍然一致。


