联合类型
联合类型从根本上不同于 Pydantic 验证的所有其他类型 - 联合类型只需要一个成员有效,而不是要求所有字段/项目/值都有效。
这导致了关于如何验证联合类型的一些细微差别
- 应该针对联合类型的哪个(哪些)成员验证数据,以及以什么顺序验证?
- 验证失败时应引发哪些错误?
验证联合类型感觉就像在验证过程中添加了另一个正交维度。
为了解决这些问题,Pydantic 支持三种基本的联合类型验证方法
- 从左到右模式 - 最简单的方法,按顺序尝试联合类型的每个成员,并返回第一个匹配项
- 智能模式 - 类似于“从左到右模式”,按顺序尝试成员;但是,验证将继续进行到第一个匹配项之后,以尝试找到更好的匹配项,这是大多数联合类型验证的默认模式
- 可区分联合类型 - 仅尝试联合类型的一个成员,基于鉴别符
提示
一般来说,我们建议使用可区分联合类型。它们比未标记的联合类型更高效且更可预测,因为它们允许您控制要针对联合类型的哪个成员进行验证。
对于复杂的情况,如果您使用未标记的联合类型,如果您需要保证针对联合类型成员的验证尝试顺序,建议使用 union_mode='left_to_right'
。
如果您正在寻找非常专业的行为,您可以使用自定义验证器。
联合模式¶
从左到右模式¶
注意
因为此模式通常会导致意外的验证结果,所以在 Pydantic >=2 中,它不是默认模式,而是 union_mode='smart'
是默认模式。
使用此方法,将按照联合类型成员定义的顺序尝试对每个成员进行验证,并且第一个成功的验证将被接受为输入。
如果所有成员的验证都失败,则验证错误将包含来自联合类型所有成员的错误。
union_mode='left_to_right'
必须在您想要使用它的联合类型字段上设置为 Field
参数。
from typing import Union
from pydantic import BaseModel, Field, ValidationError
class User(BaseModel):
id: Union[str, int] = Field(union_mode='left_to_right')
print(User(id=123))
#> id=123
print(User(id='hello'))
#> id='hello'
try:
User(id=[])
except ValidationError as e:
print(e)
"""
2 validation errors for User
id.str
Input should be a valid string [type=string_type, input_value=[], input_type=list]
id.int
Input should be a valid integer [type=int_type, input_value=[], input_type=list]
"""
from pydantic import BaseModel, Field, ValidationError
class User(BaseModel):
id: str | int = Field(union_mode='left_to_right')
print(User(id=123))
#> id=123
print(User(id='hello'))
#> id='hello'
try:
User(id=[])
except ValidationError as e:
print(e)
"""
2 validation errors for User
id.str
Input should be a valid string [type=string_type, input_value=[], input_type=list]
id.int
Input should be a valid integer [type=int_type, input_value=[], input_type=list]
"""
成员的顺序在这种情况下非常重要,如调整上述示例所示
from typing import Union
from pydantic import BaseModel, Field
class User(BaseModel):
id: Union[int, str] = Field(union_mode='left_to_right')
print(User(id=123)) # (1)
#> id=123
print(User(id='456')) # (2)
#> id=456
- 正如预期的那样,输入针对
int
成员进行验证,结果符合预期。 - 我们处于宽松模式,数字字符串
'123'
作为联合类型的第一个成员int
的输入是有效的。由于首先尝试了int
,我们得到了令人惊讶的结果,即id
是int
而不是str
。
from pydantic import BaseModel, Field
class User(BaseModel):
id: int | str = Field(union_mode='left_to_right')
print(User(id=123)) # (1)
#> id=123
print(User(id='456')) # (2)
#> id=456
- 正如预期的那样,输入针对
int
成员进行验证,结果符合预期。 - 我们处于宽松模式,数字字符串
'123'
作为联合类型的第一个成员int
的输入是有效的。由于首先尝试了int
,我们得到了令人惊讶的结果,即id
是int
而不是str
。
智能模式¶
由于 union_mode='left_to_right'
可能产生令人惊讶的结果,在 Pydantic >=2 中,Union
验证的默认模式是 union_mode='smart'
。
在此模式下,pydantic 尝试从联合类型成员中选择输入的最佳匹配项。确切的算法可能会在 Pydantic 的次要版本之间更改,以便改进性能和准确性。
注意
我们保留在未来版本的 Pydantic 中调整内部 smart
匹配算法的权利。如果您依赖于非常具体的匹配行为,建议使用 union_mode='left_to_right'
或 可区分联合类型。
智能模式算法
智能模式算法使用两个指标来确定输入的最佳匹配项
- 设置的有效字段数(与模型、数据类和类型化字典相关)
- 匹配的精确性(与所有类型相关)
设置的有效字段数¶
注意
此指标在 Pydantic v2.8.0 中引入。在此版本之前,仅使用精确性来确定最佳匹配项。
此指标目前仅与模型、数据类和类型化字典相关。
设置的有效字段数越多,匹配越好。嵌套模型上设置的字段数也会被考虑在内。这些计数会向上冒泡到顶层联合类型,其中计数最高的联合类型成员被认为是最佳匹配项。
对于此指标相关的的数据类型,我们优先考虑此计数而不是精确性。对于所有其他类型,我们仅使用精确性。
精确性¶
对于 精确性
,Pydantic 将联合类型成员的匹配项分为以下三个组之一(从最高分到最低分)
- 精确类型匹配,例如
int
输入到float | int
联合类型验证是int
成员的精确类型匹配 - 验证在
strict
模式下会成功 - 验证在宽松模式下会成功
产生最高精确性分数的联合类型匹配项将被视为最佳匹配项。
在智能模式下,将执行以下步骤来尝试选择输入的最佳匹配项
- 联合类型成员从左到右尝试,任何成功的匹配项都将按上述三个精确性类别之一进行评分,并记录设置的有效字段计数。
- 在评估完所有成员后,将返回“设置的有效字段”计数最高的成员。
- 如果“设置的有效字段”计数最高的情况下存在平局,则使用精确性分数作为决胜因素,并返回精确性分数最高的成员。
- 如果所有成员的验证都失败,则返回所有错误。
- 联合类型成员从左到右尝试,任何成功的匹配项都将按上述三个精确性类别之一进行评分。
- 如果验证在精确类型匹配中成功,则立即返回该成员,并且不会尝试后续成员。
- 如果验证在至少一个成员中作为“严格”匹配成功,则返回这些“严格”匹配中最左边的成员。
- 如果验证在“宽松”模式下的至少一个成员中成功,则返回最左边的匹配项。
- 所有成员的验证都失败,返回所有错误。
from typing import Union
from uuid import UUID
from pydantic import BaseModel
class User(BaseModel):
id: Union[int, str, UUID]
name: str
user_01 = User(id=123, name='John Doe')
print(user_01)
#> id=123 name='John Doe'
print(user_01.id)
#> 123
user_02 = User(id='1234', name='John Doe')
print(user_02)
#> id='1234' name='John Doe'
print(user_02.id)
#> 1234
user_03_uuid = UUID('cf57432e-809e-4353-adbd-9d5c0d733868')
user_03 = User(id=user_03_uuid, name='John Doe')
print(user_03)
#> id=UUID('cf57432e-809e-4353-adbd-9d5c0d733868') name='John Doe'
print(user_03.id)
#> cf57432e-809e-4353-adbd-9d5c0d733868
print(user_03_uuid.int)
#> 275603287559914445491632874575877060712
from uuid import UUID
from pydantic import BaseModel
class User(BaseModel):
id: int | str | UUID
name: str
user_01 = User(id=123, name='John Doe')
print(user_01)
#> id=123 name='John Doe'
print(user_01.id)
#> 123
user_02 = User(id='1234', name='John Doe')
print(user_02)
#> id='1234' name='John Doe'
print(user_02.id)
#> 1234
user_03_uuid = UUID('cf57432e-809e-4353-adbd-9d5c0d733868')
user_03 = User(id=user_03_uuid, name='John Doe')
print(user_03)
#> id=UUID('cf57432e-809e-4353-adbd-9d5c0d733868') name='John Doe'
print(user_03.id)
#> cf57432e-809e-4353-adbd-9d5c0d733868
print(user_03_uuid.int)
#> 275603287559914445491632874575877060712
可区分联合类型¶
可区分联合类型有时被称为“标记联合类型”。
我们可以使用可区分联合类型来更有效地验证 Union
类型,方法是选择要针对联合类型的哪个成员进行验证。
这使得验证更有效,并且还避免了验证失败时错误的激增。
向联合类型添加鉴别符也意味着生成的 JSON schema 实现了相关的 OpenAPI 规范。
具有 str
鉴别符的可区分联合类型¶
通常,在具有多个模型的 Union
的情况下,联合类型的所有成员都有一个通用字段,可用于区分应针对哪个联合类型情况验证数据;这在 OpenAPI 中称为“鉴别符”。
要基于该信息验证模型,您可以在每个模型中设置相同的字段 - 让我们称之为 my_discriminator
- 具有区分值,这是一个(或多个)Literal
值。对于您的 Union
,您可以在其值中设置鉴别符:Field(discriminator='my_discriminator')
。
from typing import Literal, Union
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: Union[Cat, Dog, Lizard] = Field(discriminator='pet_type')
n: int
print(Model(pet={'pet_type': 'dog', 'barks': 3.14}, n=1))
#> pet=Dog(pet_type='dog', barks=3.14) n=1
try:
Model(pet={'pet_type': 'dog'}, n=1)
except ValidationError as e:
print(e)
"""
1 validation error for Model
pet.dog.barks
Field required [type=missing, input_value={'pet_type': 'dog'}, input_type=dict]
"""
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(discriminator='pet_type')
n: int
print(Model(pet={'pet_type': 'dog', 'barks': 3.14}, n=1))
#> pet=Dog(pet_type='dog', barks=3.14) n=1
try:
Model(pet={'pet_type': 'dog'}, n=1)
except ValidationError as e:
print(e)
"""
1 validation error for Model
pet.dog.barks
Field required [type=missing, input_value={'pet_type': 'dog'}, input_type=dict]
"""
具有可调用 Discriminator
的可区分联合类型¶
API 文档
在具有多个模型的 Union
的情况下,有时在所有模型中没有一个统一的字段可以用作鉴别符。这是可调用 Discriminator
的完美用例。
提示
当您设计可调用鉴别符时,请记住您可能必须考虑 dict
和模型类型输入。此模式类似于 mode='before'
验证器的模式,您必须预测各种形式的输入。
但是等等!您问,我只期望传入 dict
类型,为什么我需要考虑模型?Pydantic 也将可调用鉴别符用于序列化,此时您的可调用对象的输入很可能是一个模型实例。
在以下示例中,您将看到可调用鉴别符旨在处理 dict
和模型输入。如果您不遵循此实践,则很可能在最佳情况下,您会在序列化期间收到警告,而在最坏情况下,在验证期间会收到运行时错误。
from typing import Annotated, Any, Literal, Union
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:
if isinstance(v, dict):
return v.get('fruit', v.get('filling'))
return getattr(v, 'fruit', getattr(v, 'filling', None))
class ThanksgivingDinner(BaseModel):
dessert: Annotated[
Union[
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(dessert=ApplePie(time_to_cook=60, num_ingredients=8, fruit='apple'))
"""
pumpkin_variation = ThanksgivingDinner.model_validate(
{
'dessert': {
'filling': 'pumpkin',
'time_to_cook': 40,
'num_ingredients': 6,
}
}
)
print(repr(pumpkin_variation))
"""
ThanksgivingDinner(dessert=PumpkinPie(time_to_cook=40, num_ingredients=6, filling='pumpkin'))
"""
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:
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(dessert=ApplePie(time_to_cook=60, num_ingredients=8, fruit='apple'))
"""
pumpkin_variation = ThanksgivingDinner.model_validate(
{
'dessert': {
'filling': 'pumpkin',
'time_to_cook': 40,
'num_ingredients': 6,
}
}
)
print(repr(pumpkin_variation))
"""
ThanksgivingDinner(dessert=PumpkinPie(time_to_cook=40, num_ingredients=6, filling='pumpkin'))
"""
Discriminator
也可以用于验证具有模型和原始类型组合的 Union
类型。
例如
from typing import Annotated, Any, Union
from pydantic import BaseModel, Discriminator, Tag, ValidationError
def model_x_discriminator(v: Any) -> str:
if isinstance(v, int):
return 'int'
if isinstance(v, (dict, BaseModel)):
return 'model'
else:
# return None if the discriminator value isn't found
return None
class SpecialValue(BaseModel):
value: int
class DiscriminatedModel(BaseModel):
value: Annotated[
Union[
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)
#> value=SpecialValue(value=1)
int_data = {'value': 123}
m = DiscriminatedModel.model_validate(int_data)
print(m)
#> value=123
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() [type=union_tag_not_found, input_value='not an int or a model', input_type=str]
"""
- 请注意,如果未找到鉴别符值,则可调用鉴别符函数返回
None
。当返回None
时,会引发此union_tag_not_found
错误。
from typing import Annotated, Any
from pydantic import BaseModel, Discriminator, Tag, ValidationError
def model_x_discriminator(v: Any) -> str:
if isinstance(v, int):
return 'int'
if isinstance(v, (dict, BaseModel)):
return 'model'
else:
# return None if the discriminator value isn't 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)
#> value=SpecialValue(value=1)
int_data = {'value': 123}
m = DiscriminatedModel.model_validate(int_data)
print(m)
#> value=123
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() [type=union_tag_not_found, input_value='not an int or a model', input_type=str]
"""
- 请注意,如果未找到鉴别符值,则可调用鉴别符函数返回
None
。当返回None
时,会引发此union_tag_not_found
错误。
注意
使用 注解模式 可以方便地重新组合 Union
和 discriminator
信息。有关更多详细信息,请参见下一个示例。
有几种方法可以为字段设置鉴别符,所有方法的语法略有不同。
对于 str
鉴别符
some_field: Union[...] = Field(discriminator='my_discriminator')
some_field: Annotated[Union[...], Field(discriminator='my_discriminator')]
对于可调用 Discriminator
some_field: Union[...] = Field(discriminator=Discriminator(...))
some_field: Annotated[Union[...], Discriminator(...)]
some_field: Annotated[Union[...], Field(discriminator=Discriminator(...))]
警告
可区分联合类型不能仅与单个变体一起使用,例如 Union[Cat]
。
Python 在解释时将 Union[T]
更改为 T
,因此 pydantic
无法区分 Union[T]
字段和 T
。
嵌套的可区分联合类型¶
一个字段只能设置一个鉴别符,但有时您想组合多个鉴别符。您可以通过创建嵌套的 Annotated
类型来完成,例如
from typing import Annotated, Literal, Union
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[Union[BlackCat, WhiteCat], Field(discriminator='color')]
class Dog(BaseModel):
pet_type: Literal['dog']
name: str
Pet = Annotated[Union[Cat, Dog], Field(discriminator='pet_type')]
class Model(BaseModel):
pet: Pet
n: int
m = Model(pet={'pet_type': 'cat', 'color': 'black', 'black_name': 'felix'}, n=1)
print(m)
#> pet=BlackCat(pet_type='cat', color='black', black_name='felix') n=1
try:
Model(pet={'pet_type': 'cat', 'color': 'red'}, n='1')
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' [type=union_tag_invalid, input_value={'pet_type': 'cat', 'color': 'red'}, input_type=dict]
"""
try:
Model(pet={'pet_type': 'cat', 'color': 'black'}, n='1')
except ValidationError as e:
print(e)
"""
1 validation error for Model
pet.cat.black.black_name
Field required [type=missing, input_value={'pet_type': 'cat', 'color': 'black'}, input_type=dict]
"""
提示
如果您想针对联合类型(仅限联合类型)验证数据,则可以使用 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_type='cat', color='black', black_name='felix')
联合类型验证错误¶
当 Union
验证失败时,错误消息可能会非常冗长,因为它们将为联合类型中的每种情况生成验证错误。当处理递归模型时,这一点尤其明显,在递归的每个级别都可能生成原因。在这种情况下,可区分联合类型有助于简化错误消息,因为验证错误仅针对具有匹配鉴别符值的情况生成。
您还可以通过将这些规范作为参数传递给 Discriminator
构造函数来自定义 Discriminator
的错误类型、消息和上下文,如下例所示。
from typing import Annotated, Union
from pydantic import BaseModel, Discriminator, Tag, ValidationError
# Errors are quite verbose with a normal Union:
class Model(BaseModel):
x: Union[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 [type=string_type, input_value={'x': {'x': 1}}, input_type=dict]
x.Model.x.str
Input should be a valid string [type=string_type, input_value={'x': 1}, input_type=dict]
x.Model.x.Model.x.str
Input should be a valid string [type=string_type, input_value=1, input_type=int]
x.Model.x.Model.x.Model
Input should be a valid dictionary or instance of Model [type=model_type, input_value=1, input_type=int]
"""
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 [type=string_type, input_value={'x': {'x': {}}}, input_type=dict]
x.Model.x.str
Input should be a valid string [type=string_type, input_value={'x': {}}, input_type=dict]
x.Model.x.Model.x.str
Input should be a valid string [type=string_type, input_value={}, input_type=dict]
x.Model.x.Model.x.Model.x
Field required [type=missing, input_value={}, input_type=dict]
"""
# 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[
Union[
Annotated[str, Tag('str')],
Annotated['DiscriminatedModel', Tag('model')],
],
Discriminator(
model_x_discriminator,
custom_error_type='invalid_union_member', # (1)!
custom_error_message='Invalid 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 [type=invalid_union_member, input_value=1, input_type=int]
"""
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 [type=missing, input_value={}, input_type=dict]
"""
# 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
属性。
您还可以通过使用 Tag
标记每种情况来简化错误消息。当您有复杂的类型(如本例中的类型)时,这尤其有用
from typing import Annotated, Union
from pydantic import AfterValidator, Tag, TypeAdapter, ValidationError
DoubledList = Annotated[list[int], AfterValidator(lambda x: x * 2)]
StringsMap = dict[str, str]
# Not using any `Tag`s for each union case, the errors are not so nice to look at
adapter = TypeAdapter(Union[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 [type=int_parsing, input_value='a', input_type=str]
dict[str,str]
Input should be a valid dictionary [type=dict_type, input_value=['a'], input_type=list]
"""
tag_adapter = TypeAdapter(
Union[
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 [type=int_parsing, input_value='a', input_type=str]
StringsMap
Input should be a valid dictionary [type=dict_type, input_value=['a'], input_type=list]
"""