Lewati ke isi

Serikat pekerja pada dasarnya berbeda dengan semua jenis validasi Pydantic lainnya - alih-alih mengharuskan semua bidang/item/nilai valid, serikat pekerja hanya memerlukan satu anggota untuk valid.

Hal ini menimbulkan beberapa perbedaan dalam cara memvalidasi serikat pekerja:

  • anggota serikat mana yang harus Anda validasi datanya, dan dalam urutan yang mana?
  • kesalahan apa yang muncul ketika validasi gagal?

Memvalidasi serikat pekerja terasa seperti menambahkan dimensi ortogonal lain ke dalam proses validasi.

Untuk mengatasi masalah ini, Pydantic mendukung tiga pendekatan mendasar untuk memvalidasi serikat pekerja:

  1. mode kiri ke kanan - pendekatan paling sederhana, setiap anggota serikat dicoba secara berurutan dan kecocokan pertama dikembalikan
  2. mode cerdas - mirip dengan "mode kiri ke kanan" anggota dicoba secara berurutan; namun, validasi akan dilanjutkan setelah kecocokan pertama untuk mencoba menemukan kecocokan yang lebih baik, ini adalah mode default untuk sebagian besar validasi gabungan
  3. serikat pekerja yang didiskriminasi - hanya satu anggota serikat yang diadili, berdasarkan diskriminator

Tip

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.

Mode Persatuan

Mode Kiri ke Kanan

!!! catatan Karena mode ini sering kali menghasilkan hasil validasi yang tidak diharapkan, ini bukan default di Pydantic >=2, melainkan union_mode='smart' yang default.

Dengan pendekatan ini, validasi dilakukan terhadap setiap anggota serikat sesuai urutan yang ditentukan, dan validasi pertama yang berhasil diterima sebagai masukan.

Jika validasi gagal pada semua anggota, kesalahan validasi mencakup kesalahan seluruh anggota serikat pekerja.

union_mode='left_to_right' harus ditetapkan sebagai parameter Field pada bidang gabungan tempat Anda ingin menggunakannya.

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]
    """

Urutan anggota sangat penting dalam hal ini, seperti yang ditunjukkan oleh contoh tweak di atas:

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. Seperti yang diharapkan, masukan divalidasi terhadap anggota int dan hasilnya seperti yang diharapkan.
  2. Kami berada dalam mode longgar dan string numerik '123' valid sebagai masukan ke anggota pertama serikat pekerja, int . Sejak dicoba terlebih dahulu, kami mendapatkan hasil yang mengejutkan yaitu id menjadi int dan bukannya str .

Modus Cerdas

Karena hasil yang berpotensi mengejutkan dari union_mode='left_to_right' , di Pydantic >=2 mode default untuk validasi Union adalah union_mode='smart' .

Dalam mode ini, pydantic mencoba memilih kecocokan terbaik untuk masukan dari anggota serikat pekerja. Algoritma yang tepat dapat berubah antara rilis minor Pydantic untuk memungkinkan peningkatan kinerja dan akurasi.

Catatan

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.

Algoritma Mode Cerdas

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

!!! tip Tipe Optional[x] adalah singkatan dari Union[x, None] .

See more details in [Required fields](../concepts/models.md#required-fields).

Serikat Pekerja yang Didiskriminasi

Serikat pekerja yang didiskriminasi terkadang disebut sebagai "Serikat yang Ditandai".

Kita dapat menggunakan serikat pekerja yang didiskriminasi untuk memvalidasi jenis Union secara lebih efisien, dengan memilih anggota serikat mana yang akan divalidasi.

Hal ini membuat validasi lebih efisien dan juga menghindari berkembangnya kesalahan saat validasi gagal.

Menambahkan diskriminator ke serikat pekerja juga berarti skema JSON yang dihasilkan mengimplementasikan spesifikasi OpenAPI terkait .

Serikat Pekerja yang Didiskriminasi dengan diskriminator str

Seringkali, dalam kasus Union dengan beberapa model, terdapat bidang umum untuk semua anggota serikat pekerja yang dapat digunakan untuk membedakan kasus kesatuan mana yang datanya harus divalidasi; ini disebut sebagai "diskriminator" di OpenAPI .

Untuk memvalidasi model berdasarkan informasi tersebut, Anda dapat menyetel bidang yang sama - sebut saja my_discriminator - di setiap model dengan nilai yang didiskriminasi, yaitu satu (atau banyak) nilai Literal . Untuk Union Anda, Anda dapat mengatur nilai diskriminatornya: 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]
    """

Serikat Pekerja yang Didiskriminasi dengan Discriminator yang dapat dipanggil

??? api "Dokumentasi API" [pydantic.types.Discriminator][pydantic.types.Diskriminator]

Dalam kasus Union dengan beberapa model, terkadang tidak ada satu bidang seragam di semua model yang dapat Anda gunakan sebagai pembeda. Ini adalah kasus penggunaan yang sempurna untuk Discriminator yang dapat dipanggil.

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 juga dapat digunakan untuk memvalidasi tipe Union dengan kombinasi model dan tipe primitif.

Misalnya:

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. Perhatikan fungsi diskriminator yang dapat dipanggil mengembalikan None jika nilai diskriminator tidak ditemukan. Ketika None dikembalikan, kesalahan union_tag_not_found ini muncul.

!!! catatan Menggunakan sintaksis bidang typing.Annotated dapat berguna untuk mengelompokkan kembali informasi Union dan discriminator . Lihat contoh berikutnya untuk lebih jelasnya.

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(...))]
```

!!! peringatan Serikat pekerja yang didiskriminasi tidak dapat digunakan hanya dengan satu varian, seperti 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`.

Serikat Pekerja yang Didiskriminasi

Hanya satu diskriminator yang dapat ditetapkan untuk suatu bidang, tetapi terkadang Anda ingin menggabungkan beberapa diskriminator. Anda dapat melakukannya dengan membuat tipe Annotated bersarang, misalnya:

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]
    """

!!! tip Jika Anda ingin memvalidasi data terhadap gabungan, dan hanya gabungan, Anda dapat menggunakan konstruksi TypeAdapter pydantic alih-alih mewarisi dari BaseModel standar.

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')
```

Kesalahan Validasi Serikat

Ketika validasi Union gagal, pesan kesalahan bisa sangat bertele-tele, karena akan menghasilkan kesalahan validasi untuk setiap kasus di gabungan. Hal ini terutama terlihat ketika berhadapan dengan model rekursif, di mana alasan dapat dihasilkan pada setiap tingkat rekursi. Serikat pekerja yang terdiskriminasi membantu menyederhanakan pesan kesalahan dalam kasus ini, karena kesalahan validasi hanya dihasilkan untuk kasus dengan nilai diskriminator yang cocok.

Anda juga dapat menyesuaikan jenis kesalahan, pesan, dan konteks untuk Discriminator dengan meneruskan spesifikasi ini sebagai parameter ke konstruktor Discriminator , seperti yang terlihat pada contoh di bawah.

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 adalah atribut type dari ValidationError yang dimunculkan ketika validasi gagal.
  2. custom_error_message adalah atribut msg dari ValidationError yang dimunculkan ketika validasi gagal.
  3. custom_error_context adalah atribut ctx dari ValidationError yang dimunculkan ketika validasi gagal.

Anda juga dapat menyederhanakan pesan kesalahan dengan memberi label pada setiap kasus dengan Tag. Ini sangat berguna ketika Anda memiliki tipe kompleks seperti pada contoh ini:

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]
    """

本文总阅读量