拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Pydantic Union 验证全指南:left-to-right、smart 与 discriminated 三种模式的原理与实战

Pydantic Union 验证全指南left-to-right、smart 与 discriminated 三种模式的原理与实战【免费下载链接】pydanticData validation using Python type hints项目地址: https://gitcode.com/GitHub_Trending/py/pydanticUnion 类型在 Pydantic 中是一类正交的特殊验证普通字段要求所有值都合法而 Union 只要求其中一个成员合法即可。这带来了两个核心问题——应当按什么顺序、对哪些成员尝试验证验证全部失败时应抛出哪些错误本文以当前仓库 docs/concepts/unions.md 为骨架结合 Pydantic 及 pydantic-core 源码系统讲解 Pydantic 支持的三类 Union 验证策略left_to_right模式、默认的smart模式以及基于判别器的 discriminated unions并给出可直接运行的代码示例与错误输出分析。读完本文你将能根据业务场景正确选择 Union 验证策略、规避意外匹配陷阱并利用判别器写出更高效、错误信息更可控的数据模型。为什么 Union 验证与众不同在 docs/concepts/types.md 介绍的各类类型中Union 是唯一只要一个成员通过就算通过的类型。正因如此验证 Union 会产生两类独特的取舍应该用数据去匹配 Union 中的哪一个成员按什么顺序尝试当验证失败时应该抛出哪些错误针对这些问题Pydantic 提供了三种根本不同的 Union 验证方式left-to-right 模式最简单按声明顺序逐个尝试每个成员返回第一个成功的匹配smart 模式与 left-to-right 类似按顺序尝试但会在首个匹配之后继续尝试寻找更优的匹配这是大多数 Union 验证的默认模式Pydantic 2discriminated unions判别联合通过判别器discriminator直接锁定唯一的目标成员只尝试一次。官方文档的建议很明确一般情况下优先使用 discriminated unions——它既比无标签untaggedUnion 性能更好、行为更可预测因为你可以精确控制用哪个成员去验证数据。对于复杂场景如果必须使用无标签 Union 且需要保证尝试顺序推荐使用union_modeleft_to_right。若需要极度定制化的行为可以退回到自定义验证器见 docs/concepts/validators.md。Union Modes两种无标签 Union 验证策略union_mode是Field上的一个参数取值仅为smart或left_to_right。在 pydantic/fields.py 中可以看到其类型声明union_mode: Literal[smart, left_to_right] | None该参数经 pydantic/_internal/_known_annotated_metadata.py 中的UNION_CONSTRAINTS {union_mode}注册为可识别的约束元数据当目标 schema 是union类型时apply_known_metadata会执行schema[mode] value把 Python 层的选择直接写入 core schema。最终在 pydantic-core 的 Rust 实现 pydantic-core/src/validators/union.rs 中通过UnionMode枚举Smart与LeftToRight分发到validate_smart或validate_left_to_right两个方法。Left to Right 模式注意由于该模式经常产生出人意料的验证结果它在 Pydantic 2 中不是默认值默认是union_modesmart。该模式下验证会按 Union 成员声明顺序逐个尝试第一个验证成功的成员即被接受。若所有成员都失败则错误信息会包含所有成员各自的错误。union_modeleft_to_right必须作为Field参数见 docs/concepts/fields.md设置在希望使用它的 Union 字段上from pydantic import BaseModel, Field, ValidationError class User(BaseModel): id: str | int Field(union_modeleft_to_right) print(User(id123)) # id123 print(User(idhello)) # idhello try: User(id[]) except ValidationError as e: print(e) 2 validation errors for User id.str Input should be a valid string [typestring_type, input_value[], input_typelist] id.int Input should be a valid integer [typeint_type, input_value[], input_typelist] 成员顺序至关重要在 left-to-right 模式下成员顺序会直接改变结果看下面这个微调后的例子from pydantic import BaseModel, Field class User(BaseModel): id: int | str Field(union_modeleft_to_right) print(User(id123)) # (1) # id123 print(User(id456)) # (2) # id456符合预期输入按int成员验证结果正常。我们处于 lax宽松模式数字字符串123可以作为第一个成员int的合法输入。由于int先被尝试就得到了令人惊讶的结果id被解析成了int而非str。这正是宽松模式下数字字符串可转 int与顺序敏感叠加带来的经典陷阱。源码层面pydantic-core/src/validators/union.rs 的validate_left_to_right实现会依序调用每个 choice 的验证器一旦Ok就立即返回不做任何择优。Smart Mode由于union_modeleft_to_right的潜在意外Pydantic 2 将union_modesmart设为默认值。在该模式下Pydantic 会尝试从 Union 成员中为输入挑选最合适的匹配。需要说明的是smart的具体算法可能在 Pydantic 的 minor 版本之间调整以便在性能和准确性上持续改进。注意官方保留在未来版本中调整smart内部匹配算法的权利。如果你依赖非常特定的匹配行为建议改用union_modeleft_to_right或 discriminated unions。Smart 模式算法可折叠详情smart 模式使用两个指标来决定输入的最佳匹配已设置的有效字段数量对 models、dataclasses、typed dicts 相关匹配的精确度exactness对所有类型相关。指标一已设置的有效字段数量注意该指标在 Pydantic v2.8.0 引入。此前版本只使用 exactness 判定最佳匹配。该指标目前只对 models、dataclasses 和 typed dicts 有意义。有效字段设置得越多匹配越好嵌套模型上设置的字段数也会被计入。这些计数会向上冒泡到顶层 Union拥有最高计数的成员被视为最佳匹配。对于适用该指标的数据类型计数优先于 exactness对其他所有类型则完全使用 exactness。指标二精确度exactnessPydantic 会把某个 Union 成员的匹配结果按精确度从高到低分成三档精确类型匹配例如int输入对float | intUnion 来说int成员就是精确类型匹配在strict模式下验证能成功在 lax 模式下验证能成功。得到最高精确度评分的 Union 匹配即被视为最佳匹配。smart 模式下选择最佳匹配的具体步骤对BaseModel、dataclass和TypedDictUnion 成员从左到右依次尝试成功的匹配会被归入上述三档精确度之一同时统计有效字段设置计数所有成员评估完毕后返回有效字段设置计数最高的成员若有效字段设置计数打平则用精确度评分作 tiebreaker返回精确度最高的成员若所有成员都验证失败则返回全部错误。对其他所有数据类型Union 成员从左到右依次尝试成功的匹配归入三档精确度之一若出现精确类型匹配立即返回该成员不再尝试后续成员若至少一个成员以 strict 匹配成功返回其中最靠左的 strict 匹配若至少一个成员以 lax 模式成功返回其中最靠左的匹配若所有成员都验证失败返回全部错误。Rust 端 pydantic-core/src/validators/union.rs 的validate_smart完整实现了上述逻辑(Some(Exactness::Exact), None)组合精确匹配且无字段计数时立即返回否则把(成功值, exactness, fields_set_count)记入best_match比较时字段计数优先、exactness 作 tiebreaker与文档描述完全一致。Smart 模式实战示例from uuid import UUID from pydantic import BaseModel class User(BaseModel): id: int | str | UUID name: str user_01 User(id123, nameJohn Doe) print(user_01) # id123 nameJohn Doe print(user_01.id) # 123 user_02 User(id1234, nameJohn Doe) print(user_02) # id1234 nameJohn Doe print(user_02.id) # 1234 user_03_uuid UUID(cf57432e-809e-4353-adbd-9d5c0d733868) user_03 User(iduser_03_uuid, nameJohn Doe) print(user_03) # idUUID(cf57432e-809e-4353-adbd-9d5c0d733868) nameJohn Doe print(user_03.id) # cf57432e-809e-4353-adbd-9d5c0d733868 print(user_03_uuid.int) # 275603287559914445491632874575877060712注意与 left-to-right 的差别id1234这个字符串输入在 smart 模式下没有被转成 int而是保留了str类型因为对str成员而言这是精确类型匹配会被优先选中。Discriminated Unions判别联合判别联合有时也被称为Tagged unions标签联合。判别联合可以更高效地完成验证通过判别器精确指定用 Union 中的哪个成员去验证数据。这不仅提升了验证性能还能避免验证失败时错误信息爆炸式增长。此外给 Union 添加判别器后生成的 JSON schema 会实现 OpenAPI 规范中的discriminator属性。使用字符串判别器的判别联合在包含多个模型的 Union 场景中各成员通常存在一个公共字段可以用来判断数据应该按哪个 Union 分支验证。为此可以在每个模型上设置一个公共字段下例中的pet_type其类型为一个或多个Literal值定义判别联合类型时必须通过Field()函数见 docs/concepts/fields.md的discriminator参数指定也可以使用Discriminator类型from typing import Literal from pydantic import BaseModel, Field, ValidationError class Cat(BaseModel): pet_type: Literal[cat] meows: int class Dog(BaseModel): pet_type: Literal[dog] barks: float class Lizard(BaseModel): pet_type: Literal[reptile, lizard] scales: bool class Model(BaseModel): pet: Cat | Dog | Lizard Field(discriminatorpet_type) n: int print(Model(pet{pet_type: dog, barks: 3.14}, n1)) # petDog(pet_typedog, barks3.14) n1 try: Model(pet{pet_type: dog}, n1) except ValidationError as e: print(e) 1 validation error for Model pet.dog.barks Field required [typemissing, input_value{pet_type: dog}, input_typedict] 注意Lizard的pet_type是Literal[reptile, lizard]——一个成员可以对应多个判别值。当输入判别值确定后Pydantic 只会验证对应的那一个成员错误也只会定位到该分支上例中pet_type: dog但缺少barks字段时只报pet.dog.barks一个错误。从 Pydantic v2.13 起root 类型为Literal的 Root 模型见 docs/concepts/models.md 中的 RootModel 与自定义 root 类型可以替代Literal类型用于判别场景。使用可调用Discriminator的判别联合API 文档pydantic.types.Discriminator当 Union 的多个模型没有一个统一的公共字段可作判别器时可调用Discriminator就是理想方案。提示设计可调用判别器时务必同时处理dict和模型类型两种输入。这与modebefore验证器相似——你需要预判各种输入形态。你可能会问我只打算传dict为什么还要考虑模型因为 Pydantic 在序列化时也会调用可调用判别器此时传入的极可能是模型实例。下面例子中的判别器都同时兼容dict与模型输入若不这样做轻则在序列化时产生警告重则在验证阶段直接出现运行时错误。from typing import Annotated, Any, Literal from pydantic import BaseModel, Discriminator, Tag class Pie(BaseModel): time_to_cook: int num_ingredients: int class ApplePie(Pie): fruit: Literal[apple] apple class PumpkinPie(Pie): filling: Literal[pumpkin] pumpkin def get_discriminator_value(v: Any) - str | None: if isinstance(v, dict): return v.get(fruit, v.get(filling)) return getattr(v, fruit, getattr(v, filling, None)) class ThanksgivingDinner(BaseModel): dessert: Annotated[ Annotated[ApplePie, Tag(apple)] | Annotated[PumpkinPie, Tag(pumpkin)], Discriminator(get_discriminator_value), ] apple_variation ThanksgivingDinner.model_validate( {dessert: {fruit: apple, time_to_cook: 60, num_ingredients: 8}} ) print(repr(apple_variation)) ThanksgivingDinner(dessertApplePie(time_to_cook60, num_ingredients8, fruitapple)) pumpkin_variation ThanksgivingDinner.model_validate( { dessert: { filling: pumpkin, time_to_cook: 40, num_ingredients: 6, } } ) print(repr(pumpkin_variation)) ThanksgivingDinner(dessertPumpkinPie(time_to_cook40, num_ingredients6, fillingpumpkin)) Discriminator还可以用于验证模型与基本类型混合的 Union。例如from typing import Annotated, Any from pydantic import BaseModel, Discriminator, Tag, ValidationError def model_x_discriminator(v: Any) - str | None: if isinstance(v, int): return int if isinstance(v, (dict, BaseModel)): return model else: # return None if the discriminator value isnt found return None class SpecialValue(BaseModel): value: int class DiscriminatedModel(BaseModel): value: Annotated[ Annotated[int, Tag(int)] | Annotated[SpecialValue, Tag(model)], Discriminator(model_x_discriminator), ] model_data {value: {value: 1}} m DiscriminatedModel.model_validate(model_data) print(m) # valueSpecialValue(value1) int_data {value: 123} m DiscriminatedModel.model_validate(int_data) print(m) # value123 try: DiscriminatedModel.model_validate({value: not an int or a model}) except ValidationError as e: print(e) # (1)! 1 validation error for DiscriminatedModel value Unable to extract tag using discriminator model_x_discriminator() [typeunion_tag_not_found, input_valuenot an int or a model, input_typestr] 注意可调用判别器在找不到判别值时返回None一旦返回None就会抛出union_tag_not_found错误。源码中的Tag与Discriminator在 pydantic/types.py 中可以找到这两个核心类型Tag是一个slotsTrue, frozenTrue的 dataclass仅含一个字段tag: str。它的作用是为可调用判别联合的每个分支指定期望标签同时也能给 Union 分支打上错误信息中的人类可读标签。其__get_pydantic_core_schema__会把标签写入 core schema 的metadata[pydantic_internal_union_tag_key]供判别器匹配。注意使用可调用Discriminator时每个分支都必须指定Tag否则会抛出PydanticUserError错误码callable-discriminator-no-tag见 docs/errors/usage_errors.md。Discriminator同样是slotsTrue, frozenTrue的 dataclass核心字段为discriminator: str | Callable[[Any], Hashable]——字符串形式表示字段名可调用形式则从输入中提取判别值。它还支持三个自定义错误参数custom_error_type、custom_error_message、custom_error_context详见下文Union 验证错误一节。其 schema 转换逻辑会把普通 union schema 转换为TaggedUnionSchema见_convert_schemapydantic/types.py。在 pydantic-core 侧TaggedUnionValidatorpydantic-core/src/validators/union.rs负责按标签直接选择目标成员验证序列化时则由TaggedUnionSerializerpydantic-core/src/serializers/type_serializers/union.rs处理这也是前文提示判别器需兼容模型输入的原因。仓库测试 tests/types/unions/test_discriminated_union.py 覆盖了判别联合在模型、TypedDict、dataclass、递归类型、序列化等场景下的行为。设置判别器的多种语法注意使用 annotated pattern注解模式可以很方便地把 Union 与判别器信息组织在一起。下面列举设置判别器的几种语法它们在写法上略有差异。字符串判别器some_field: ... | ... Field(discriminatormy_discriminator) some_field: Annotated[... | ..., Field(discriminatormy_discriminator)]可调用Discriminatorsome_field: ... | ... Field(discriminatorDiscriminator(...)) some_field: Annotated[... | ..., Discriminator(...)] some_field: Annotated[... | ..., Field(discriminatorDiscriminator(...))]嵌套判别联合一个字段只能设置一个判别器但有时你需要组合多个判别器。可以通过创建嵌套的Annotated类型来实现例如from typing import Annotated, Literal from pydantic import BaseModel, Field, ValidationError class BlackCat(BaseModel): pet_type: Literal[cat] color: Literal[black] black_name: str class WhiteCat(BaseModel): pet_type: Literal[cat] color: Literal[white] white_name: str Cat Annotated[BlackCat | WhiteCat, Field(discriminatorcolor)] class Dog(BaseModel): pet_type: Literal[dog] name: str Pet Annotated[Cat | Dog, Field(discriminatorpet_type)] class Model(BaseModel): pet: Pet n: int m Model(pet{pet_type: cat, color: black, black_name: felix}, n1) print(m) # petBlackCat(pet_typecat, colorblack, black_namefelix) n1 try: Model(pet{pet_type: cat, color: red}, n1) except ValidationError as e: print(e) 1 validation error for Model pet.cat Input tag red found using color does not match any of the expected tags: black, white [typeunion_tag_invalid, input_value{pet_type: cat, color: red}, input_typedict] try: Model(pet{pet_type: cat, color: black}, n1) except ValidationError as e: print(e) 1 validation error for Model pet.cat.black.black_name Field required [typemissing, input_value{pet_type: cat, color: black}, input_typedict] 这里先通过color判别Cat内部的BlackCat/WhiteCat再通过pet_type判别Pet层面的Cat/Dog形成两层判别链。错误信息中可以看到判别失败时的union_tag_invalid错误类型以及定位到具体分支的字段缺失错误。提示如果你只想验证一个 Union、且只针对 Union 本身可以使用 Pydantic 的TypeAdapter结构而无需继承BaseModel。以上面示例为例type_adapter TypeAdapter(Pet) pet type_adapter.validate_python( {pet_type: cat, color: black, black_name: felix} ) print(repr(pet)) # BlackCat(pet_typecat, colorblack, black_namefelix)Union 验证错误从错误爆炸到精确报告当 Union 验证失败时错误信息可能相当冗长因为系统会为 Union 中的每一个分支都生成验证错误。在递归模型中尤其明显——每一层递归都可能产生错误原因。判别联合恰恰能在这种情况下简化错误信息因为只会为判别值匹配的那个分支生成验证错误。解读一次 Union 失败本质上是要判断哪个成员本应匹配。一个ValidationError会包含每个成员的错误与被拒绝的值当失败发生在已部署的服务中时Logfire 会在相关 trace 中保留这些细节供事后对比分析。自定义Discriminator的错误类型、消息与上下文你可以通过向Discriminator构造函数传入参数自定义其抛出的错误类型type、消息msg与上下文ctx见下例from typing import Annotated from pydantic import BaseModel, Discriminator, Tag, ValidationError # Errors are quite verbose with a normal union: class Model(BaseModel): x: str | Model try: Model.model_validate({x: {x: {x: 1}}}) except ValidationError as e: print(e) 4 validation errors for Model x.str Input should be a valid string [typestring_type, input_value{x: {x: 1}}, input_typedict] x.Model.x.str Input should be a valid string [typestring_type, input_value{x: 1}, input_typedict] x.Model.x.Model.x.str Input should be a valid string [typestring_type, input_value1, input_typeint] x.Model.x.Model.x.Model Input should be a valid dictionary or instance of Model [typemodel_type, input_value1, input_typeint] try: Model.model_validate({x: {x: {x: {}}}}) except ValidationError as e: print(e) 4 validation errors for Model x.str Input should be a valid string [typestring_type, input_value{x: {x: {}}}, input_typedict] x.Model.x.str Input should be a valid string [typestring_type, input_value{x: {}}, input_typedict] x.Model.x.Model.x.str Input should be a valid string [typestring_type, input_value{}, input_typedict] x.Model.x.Model.x.Model.x Field required [typemissing, input_value{}, input_typedict] # Errors are much simpler with a discriminated union: def model_x_discriminator(v): if isinstance(v, str): return str if isinstance(v, (dict, BaseModel)): return model class DiscriminatedModel(BaseModel): x: Annotated[ Annotated[str, Tag(str)] | Annotated[DiscriminatedModel, Tag(model)], Discriminator( model_x_discriminator, custom_error_typeinvalid_union_member, # (1)! custom_error_messageInvalid union member, # (2)! custom_error_context{discriminator: str_or_model}, # (3)! ), ] try: DiscriminatedModel.model_validate({x: {x: {x: 1}}}) except ValidationError as e: print(e) 1 validation error for DiscriminatedModel x.model.x.model.x Invalid union member [typeinvalid_union_member, input_value1, input_typeint] try: DiscriminatedModel.model_validate({x: {x: {x: {}}}}) except ValidationError as e: print(e) 1 validation error for DiscriminatedModel x.model.x.model.x.model.x Field required [typemissing, input_value{}, input_typedict] # The data is still handled properly when valid: data {x: {x: {x: a}}} m DiscriminatedModel.model_validate(data) print(m.model_dump()) # {x: {x: {x: a}}}custom_error_type是验证失败时抛出的ValidationError的type属性值。custom_error_message是验证失败时抛出的ValidationError的msg属性值。custom_error_context是验证失败时抛出的ValidationError的ctx属性值。对比可见普通递归 Union 的失败会产生 4 条层层嵌套的错误而判别联合在同一输入下只产生 1 条错误且递归定位到最深层x.model.x.model.x错误信息被大幅压缩。用Tag简化复杂类型的错误标签你还可以通过给每个分支打Tag标签来简化错误信息这在分支类型很复杂时尤其有用from typing import Annotated from pydantic import AfterValidator, Tag, TypeAdapter, ValidationError DoubledList Annotated[list[int], AfterValidator(lambda x: x * 2)] StringsMap dict[str, str] # Not using any Tags for each union case, the errors are not so nice to look at adapter TypeAdapter(DoubledList | StringsMap) try: adapter.validate_python([a]) except ValidationError as exc_info: print(exc_info) 2 validation errors for union[function-after[lambda(), list[int]],dict[str,str]] function-after[lambda(), list[int]].0 Input should be a valid integer, unable to parse string as an integer [typeint_parsing, input_valuea, input_typestr] dict[str,str] Input should be a valid dictionary [typedict_type, input_value[a], input_typelist] tag_adapter TypeAdapter( Annotated[DoubledList, Tag(DoubledList)] | Annotated[StringsMap, Tag(StringsMap)] ) try: tag_adapter.validate_python([a]) except ValidationError as exc_info: print(exc_info) 2 validation errors for union[DoubledList,StringsMap] DoubledList.0 Input should be a valid integer, unable to parse string as an integer [typeint_parsing, input_valuea, input_typestr] StringsMap Input should be a valid dictionary [typedict_type, input_value[a], input_typelist] 未打标签时Union 在错误标题中显示为union[function-after[lambda(), list[int]],dict[str,str]]内部 lambda 表达式难以阅读打上Tag后标题变为union[DoubledList,StringsMap]各分支错误也以DoubledList.0、StringsMap这样的可读名称呈现。如何选择 Union 验证策略综合全文可以给出如下选型建议场景推荐策略理由各模型存在可区分彼此的公共Literal字段字符串判别器Field(discriminator...)性能最好、行为最可预测、错误信息最少且 JSON schema 原生支持 OpenAPIdiscriminator无公共字段或混合模型与基本类型可调用DiscriminatorTag用自定义逻辑提取标签享受判别联合的性能与简洁错误记得兼容dict与模型输入需要多层判别多个判别维度嵌套Annotated每层一个判别器逐层收敛目标成员需要保证尝试顺序且可接受顺序敏感union_modeleft_to_right严格按声明顺序取第一个成功匹配注意 lax 模式下数字字符串会被转成前面的数字类型默认情况union_modesmart无需显式设置兼顾类型精确度与字段匹配数对大多数场景给出合理结果但算法细节可能在 minor 版本间微调对于只想验证 Union 本身、不关心模型容器的场景直接使用TypeAdapter即可。若需要更细粒度的自定义逻辑可参考 docs/concepts/validators.md 中的字段验证器。相关实现与测试可在 pydantic-core/src/validators/union.rs、pydantic/types.py 与 tests/types/unions/test_discriminated_union.py 中进一步研读。【免费下载链接】pydanticData validation using Python type hints项目地址: https://gitcode.com/GitHub_Trending/py/pydantic创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门