Объединения
Объединения фундаментально отличаются от всех других типов проверки Pydantic: вместо того, чтобы требовать, чтобы все поля/элементы/значения были действительными, объединения требуют, чтобы действительным был только один член.
Это приводит к некоторым нюансам в отношении проверки объединений:
- с каким членом(ами) союза следует проверять данные и в каком порядке?
- какие ошибки вызывать при неудачной проверке?
Проверка объединений напоминает добавление еще одного ортогонального измерения к процессу проверки.
Чтобы решить эти проблемы, Pydantic поддерживает три фундаментальных подхода к проверке объединений:
- режим слева направо — самый простой подход, каждый член союза проверяется по порядку и возвращается первое совпадение
- интеллектуальный режим — аналогично режиму «слева направо», участники опробуются по порядку; однако проверка будет продолжена после первого совпадения, чтобы попытаться найти лучшее совпадение. Это режим по умолчанию для большинства проверок объединения.
- дискриминируемые профсоюзы - судят только одного члена профсоюза по признаку дискриминатора
Кончик
In general, we recommend using discriminated unions. They are both more performant and more predictable than untagged unions, as they allow you to control which member of the union to validate against.
For complex cases, if you're using untagged unions, it's recommended to use union_mode='left_to_right'
if you need guarantees about the order of validation attempts against the union members.
If you're looking for incredibly specialized behavior, you can use a custom validator.
Режимы Союза¶
Режим слева направо¶
!!! note Примечание. Поскольку этот режим часто приводит к неожиданным результатам проверки, в 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 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
. Поскольку это проверяется в первую очередь, мы получаем неожиданный результат:id
являетсяint
вместоstr
.
Умный режим¶
Из-за потенциально неожиданных результатов union_mode='left_to_right'
в Pydantic >=2 режимом проверки Union
по умолчанию является union_mode='smart'
.
В этом режиме pydantic пытается выбрать наилучшее соответствие входным данным членов объединения. Точный алгоритм может меняться между второстепенными выпусками Pydantic, чтобы обеспечить улучшение как производительности, так и точности.
Примечание
We reserve the right to tweak the internal smart
matching algorithm in future versions of Pydantic. If you rely on very specific
matching behavior, it's recommended to use union_mode='left_to_right'
or discriminated unions.
??? информация «Алгоритм интеллектуального режима»
The smart mode algorithm uses two metrics to determine the best match for the input:
1. The number of valid fields set (relevant for models, dataclasses, and typed dicts)
2. The exactness of the match (relevant for all types)
#### Number of valid fields set
!!! note
This metric was introduced in Pydantic v2.8.0. Prior to this version, only exactness was used to determine the best match.
This metric is currently only relevant for models, dataclasses, and typed dicts.
The greater the number of valid fields set, the better the match. The number of fields set on nested models is also taken into account.
These counts bubble up to the top-level union, where the union member with the highest count is considered the best match.
For data types where this metric is relevant, we prioritize this count over exactness. For all other types, we use solely exactness.
#### Exactness
For `exactness`, Pydantic scores a match of a union member into one of the following three groups (from highest score to lowest score):
- An exact type match, for example an `int` input to a `float | int` union validation is an exact type match for the `int` member
- Validation would have succeeded in [`strict` mode](../concepts/strict_mode.md)
- Validation would have succeeded in lax mode
The union match which produced the highest exactness score will be considered the best match.
In smart mode, the following steps are taken to try to select the best match for the input:
=== "`BaseModel`, `dataclass`, and `TypedDict`"
1. Union members are attempted left to right, with any successful matches scored into one of the three exactness categories described above,
with the valid fields set count also tallied.
2. After all members have been evaluated, the member with the highest "valid fields set" count is returned.
3. If there's a tie for the highest "valid fields set" count, the exactness score is used as a tiebreaker, and the member with the highest exactness score is returned.
4. If validation failed on all the members, return all the errors.
=== "All other data types"
1. Union members are attempted left to right, with any successful matches scored into one of the three exactness categories described above.
- If validation succeeds with an exact type match, that member is returned immediately and following members will not be attempted.
2. If validation succeeded on at least one member as a "strict" match, the leftmost of those "strict" matches is returned.
3. If validation succeeded on at least one member in "lax" mode, the leftmost match is returned.
4. Validation failed on all the members, return all the errors.
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
!!! Подсказка Тип Optional[x]
является сокращением от Union[x, None]
.
See more details in [Required fields](../concepts/models.md#required-fields).
Дискриминированные профсоюзы¶
Дискриминированные профсоюзы иногда называют «Тегированными союзами».
Мы можем использовать дискриминируемые объединения для более эффективной проверки типов Union
, выбирая, по какому члену объединения проводить проверку.
Это делает проверку более эффективной, а также позволяет избежать увеличения количества ошибок в случае сбоя проверки.
Добавление дискриминатора в объединения также означает, что сгенерированная схема JSON реализует соответствующую спецификацию 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]
"""
Дискриминированные объединения с вызываемым Discriminator
¶
??? API "Документация по API" [pydantic.types.Discriminator
][pydantic.types.Дискриминатор]
В случае Union
с несколькими моделями иногда во всех моделях нет единого поля, которое можно использовать в качестве дискриминатора. Это идеальный вариант использования вызываемого Discriminator
.
from typing import Any, Literal, Union
from typing_extensions import Annotated
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'))
"""
Discriminator
также можно использовать для проверки типов Union
с помощью комбинаций моделей и примитивных типов.
Например:
from typing import Any, Union
from typing_extensions import Annotated
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
.
!!! Примечание. Использование синтаксиса полей typing.Annotated
может оказаться полезным для перегруппировки информации Union
и discriminator
. Более подробную информацию смотрите в следующем примере.
There are a few ways to set a discriminator for a field, all varying slightly in syntax.
For `str` discriminators:
```
some_field: Union[...] = Field(discriminator='my_discriminator'
some_field: Annotated[Union[...], Field(discriminator='my_discriminator')]
```
For callable `Discriminator`s:
```
some_field: Union[...] = Field(discriminator=Discriminator(...))
some_field: Annotated[Union[...], Discriminator(...)]
some_field: Annotated[Union[...], Field(discriminator=Discriminator(...))]
```
!!! предупреждение. Размеченные союзы нельзя использовать только с одним вариантом, например Union[Cat]
.
Python changes `Union[T]` into `T` at interpretation time, so it is not possible for `pydantic` to
distinguish fields of `Union[T]` from `T`.
Вложенные дискриминационные союзы¶
Для поля можно установить только один дискриминатор, но иногда требуется объединить несколько дискриминаторов. Вы можете сделать это, создав вложенные Annotated
типы, например:
from typing import Literal, Union
from typing_extensions import Annotated
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]
"""
!!! Совет Если вы хотите проверить данные на соответствие объединению и только объединению, вы можете использовать конструкцию TypeAdapter
pydantic вместо наследования от стандартной BaseModel
.
In the context of the previous example, we have the following:
```python
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 Union
from typing_extensions import Annotated
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
— это атрибутtype
ValidationError
возникающий при сбое проверки.custom_error_message
— это атрибутmsg
ValidationError
возникающий при сбое проверки.custom_error_context
— это атрибутctx
ValidationError
, возникающий в случае сбоя проверки.
Вы также можете упростить сообщения об ошибках, пометив каждый случай Tag
. Это особенно полезно, когда у вас есть сложные типы, подобные приведенным в этом примере:
from typing import Dict, List, Union
from typing_extensions 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 `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]
"""
本文总阅读量次