コンテンツにスキップ

ユニオンタイプ

ユニオンは、Pydantic が検証する他のすべての型とは根本的に異なります。ユニオンでは、すべてのフィールド/項目/値が有効である必要があるのではなく、1 つのメンバーのみが有効である必要があります。

これにより、共用体を検証する方法に関していくつかの微妙な違いが生じます。

  • ユニオンのどのメンバーに対して、どの順序でデータを検証する必要がありますか?
  • 検証が失敗した場合にどのエラーが発生するでしょうか?

ユニオンの検証は、検証プロセスに別の直交する次元を追加するような感じです。

これらの問題を解決するために、Pydantic は共用体を検証するための 3 つの基本的なアプローチをサポートしています。

  1. 左から右モード- 最も単純なアプローチ。共用体の各メンバーが順番に試行され、最初に一致したものが返されます。
  2. スマート モード- 「左から右モード」と同様に、メンバーが順番に試行されます。ただし、検証は最初の一致を超えて続行され、より適切な一致を見つけようとします。これが、ほとんどの共用体検証のデフォルト モードです。
  3. 区別されたユニオン- 差別子に基づいて、ユニオンの 1 つのメンバーだけが試行されます

ヒント

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.

ユニオンモード

左から右モード

!!!このモードは予期しない検証結果を引き起こすことが多いため、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
  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.

??? info「スマートモードアルゴリズム」

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の場合、データを検証する必要のある Union ケースを区別するために使用できる、Union のすべてのメンバーに共通のフィールドがあります。これは、 OpenAPIでは「識別子」と呼ばれます。

その情報に基づいてモデルを検証するには、各モデルに同じフィールド ( my_discriminatorと呼びます) に識別値 (1 つ (または複数) の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

複数のモデルを含む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`.

ネストされた差別ユニオン

1 つのフィールドに設定できる識別子は 1 つだけですが、複数の識別子を組み合わせたい場合があります。これは、ネストされた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]
    """

!!!ヒント データを共用体に対して検証したい場合、および共用体のみを検証したい場合は、標準の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検証が失敗すると、ユニオン内のケースごとに検証エラーが生成されるため、エラー メッセージが非常に冗長になることがあります。これは、再帰の各レベルで理由が生成される可能性がある再帰モデルを扱う場合に特に顕著です。この場合、識別子値が一致する場合にのみ検証エラーが生成されるため、識別共用体はエラー メッセージを簡素化するのに役立ちます。

以下の例に示すように、これらの仕様をパラメーターとして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]
    """

本文总阅读量