Union은 Pydantic이 검증하는 다른 모든 유형과 근본적으로 다릅니다. 모든 필드/항목/값이 유효하도록 요구하는 대신 Union은 단 하나의 멤버만 유효하도록 요구합니다.
이로 인해 공용체의 유효성을 검사하는 방법에 대한 약간의 차이가 발생합니다.
- Union의 어떤 구성원을 기준으로 데이터를 검증해야 하며, 어떤 순서로 검증해야 합니까?
- 유효성 검사가 실패하면 어떤 오류가 발생합니까?
공용체의 유효성을 검사하는 것은 유효성 검사 프로세스에 또 다른 직교 차원을 추가하는 것과 같습니다.
이러한 문제를 해결하기 위해 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'
사용하려는 Union 필드의 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
멤버에 대해 검증되고 결과는 예상대로입니다. - 우리는 lax 모드에 있으며 숫자 문자열
'123'
공용체의 첫 번째 멤버int
에 대한 입력으로 유효합니다. 이것이 먼저 시도되었기 때문에id
str
대신int
라는 놀라운 결과를 얻습니다.
스마트 모드¶
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:
- The number of valid fields set (relevant for models, dataclasses, and typed dicts)
- 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 afloat | int
union validation is an exact type match for theint
member - Validation would have succeeded in
strict
mode - 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:
- 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.
- After all members have been evaluated, the member with the highest "valid fields set" count is returned.
- 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.
- If validation failed on all the members, return all the errors.
- 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.
- If validation succeeded on at least one member as a "strict" match, the leftmost of those "strict" matches is returned.
- If validation succeeded on at least one member in "lax" mode, the leftmost match is returned.
- 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
의 경우 데이터의 유효성을 검사해야 하는 Union 사례를 구별하는 데 사용할 수 있는 Union의 모든 구성원에 대한 공통 필드가 있는 경우가 많습니다. OpenAPI 에서는 이를 "판별자"라고 합니다.
해당 정보를 기반으로 모델의 유효성을 검사하려면 식별된 값(하나 이상의 Literal
값)을 사용하여 각 모델에 동일한 필드( my_discriminator
라고 함)를 설정할 수 있습니다. 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
여러 모델이 포함된 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
오류가 발생합니다.
!!! note 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]
"""
!!! 팁 Union에 대해서만 데이터의 유효성을 검사하려면 표준 BaseModel
에서 상속하는 대신 pydantic의 TypeAdapter
구성을 사용할 수 있습니다.
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 검증 오류¶
Union
유효성 검사가 실패하면 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
은 유효성 검사가 실패할 때 발생하는ValidationError
의type
속성입니다.custom_error_message
는 유효성 검사가 실패할 때 발생하는ValidationError
의msg
속성입니다.custom_error_context
는 유효성 검사가 실패할 때 발생하는ValidationError
의ctx
속성입니다.
각 사례에 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]
"""
本文总阅读量次