콘텐츠로 이동

Union은 Pydantic이 검증하는 다른 모든 유형과 근본적으로 다릅니다. 모든 필드/항목/값이 유효하도록 요구하는 대신 Union은 단 하나의 멤버만 유효하도록 요구합니다.

이로 인해 공용체의 유효성을 검사하는 방법에 대한 약간의 차이가 발생합니다.

  • Union의 어떤 구성원을 기준으로 데이터를 검증해야 하며, 어떤 순서로 검증해야 합니까?
  • 유효성 검사가 실패하면 어떤 오류가 발생합니까?

공용체의 유효성을 검사하는 것은 유효성 검사 프로세스에 또 다른 직교 차원을 추가하는 것과 같습니다.

이러한 문제를 해결하기 위해 Pydantic은 공용체 검증에 대한 세 가지 기본 접근 방식을 지원합니다.

  1. 왼쪽에서 오른쪽 모드 - 가장 간단한 접근 방식으로, 공용체의 각 구성원이 순서대로 시도되고 첫 번째 일치 항목이 반환됩니다.
  2. 스마트 모드 - "왼쪽에서 오른쪽 모드"와 유사하게 멤버가 순서대로 시도됩니다. 그러나 유효성 검사는 더 나은 일치 항목을 찾기 위해 첫 번째 일치 항목을 지나 계속 진행됩니다. 이는 대부분의 통합 유효성 검사에 대한 기본 모드입니다.
  3. 차별된 조합 - 차별자에 따라 조합의 한 구성원만 재판을 받습니다.

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
  1. 예상대로 입력은 int 멤버에 대해 검증되고 결과는 예상대로입니다.
  2. 우리는 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:

  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
  • 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:

  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.
  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 의 경우 데이터의 유효성을 검사해야 하는 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]
    """
  1. 호출 가능한 판별자 함수는 판별자 값을 찾을 수 없는 경우 None 반환합니다. None 이 반환되면 이 union_tag_not_found 오류가 발생합니다.

!!! note typing.Annotated 필드 구문을 사용하면 Uniondiscriminator 정보를 재그룹화하는 데 편리할 수 있습니다. 자세한 내용은 다음 예를 참조하세요.

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'}}}
  1. custom_error_type 은 유효성 검사가 실패할 때 발생하는 ValidationErrortype 속성입니다.
  2. custom_error_message 는 유효성 검사가 실패할 때 발생하는 ValidationErrormsg 속성입니다.
  3. custom_error_context 는 유효성 검사가 실패할 때 발생하는 ValidationErrorctx 속성입니다.

각 사례에 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]
    """

本文总阅读量