Aller au contenu

Les unions sont fondamentalement différentes de tous les autres types de validations Pydantic - au lieu d'exiger que tous les champs/éléments/valeurs soient valides, les unions exigent qu'un seul membre soit valide.

Cela conduit à quelques nuances sur la manière de valider les syndicats:

  • contre quel(s) membre(s) du syndicat devriez-vous valider les données et dans quel ordre?
  • quelles erreurs générer en cas d'échec de la validation?

Valider les syndicats revient à ajouter une autre dimension orthogonale au processus de validation.

Pour résoudre ces problèmes, Pydantic prend en charge trois approches fondamentales pour valider les syndicats:

  1. mode de gauche à droite - l'approche la plus simple, chaque membre du syndicat est jugé dans l'ordre et la première correspondance est renvoyée
  2. mode intelligent - similaire au « mode de gauche à droite », les membres sont essayés dans l'ordre; cependant, la validation se poursuivra au-delà de la première correspondance pour tenter de trouver une meilleure correspondance. Il s'agit du mode par défaut pour la plupart des validations syndicales.
  3. syndicats discriminés - un seul membre du syndicat est jugé, sur la base d'un discriminateur

Conseil

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.

Modes syndicaux

Mode gauche à droite

!!! note Étant donné que ce mode conduit souvent à des résultats de validation inattendus, ce n'est pas la valeur par défaut dans Pydantic >=2, mais union_mode='smart' est la valeur par défaut.

Avec cette approche, la validation est tentée contre chaque membre du syndicat dans l'ordre dans lequel ils sont définis, et la première validation réussie est acceptée comme entrée.

Si la validation échoue sur tous les membres, l'erreur de validation inclut les erreurs de tous les membres du syndicat.

union_mode='left_to_right' doit être défini comme paramètre Field sur les champs union où vous souhaitez l'utiliser.

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

L'ordre des membres est très important dans ce cas, comme le démontre l'exemple ci-dessus:

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. Comme prévu, l'entrée est validée par rapport au membre int et le résultat est comme prévu.
  2. Nous sommes en mode laxiste et la chaîne numérique '123' est valide comme entrée pour le premier membre du syndicat, int . Puisque cela est essayé en premier, nous obtenons le résultat surprenant de id étant un int au lieu d'un str .

Mode intelligent

En raison des résultats potentiellement surprenants de union_mode='left_to_right' , dans Pydantic >=2, le mode par défaut pour la validation Union est union_mode='smart' .

Dans ce mode, pydantic tente de sélectionner la meilleure correspondance pour les contributions des membres du syndicat. L'algorithme exact peut changer entre les versions mineures de Pydantic pour permettre des améliorations en termes de performances et de précision.

Note

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.

Algorithme du mode intelligent

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

!!! astuce Le type Optional[x] est un raccourci pour Union[x, None] .

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

Des syndicats discriminés

Les syndicats discriminés sont parfois appelés «syndicats étiquetés».

Nous pouvons utiliser les syndicats discriminés pour valider plus efficacement les types Union , en choisissant contre quel membre du syndicat valider.

Cela rend la validation plus efficace et évite également une prolifération d'erreurs en cas d'échec de la validation.

L'ajout d'un discriminateur aux unions signifie également que le schéma JSON généré implémente la spécification OpenAPI associée .

Syndicats discriminés avec des discriminateurs str

Souvent, dans le cas d'une Union comportant plusieurs modèles, il existe un champ commun à tous les membres de l'union qui peut être utilisé pour distinguer le cas d'union par rapport auquel les données doivent être validées; c'est ce qu'on appelle le « discriminateur » dans OpenAPI .

Pour valider les modèles sur la base de ces informations, vous pouvez définir le même champ - appelons-le my_discriminator - dans chacun des modèles avec une valeur discriminée, qui est une (ou plusieurs) valeur(s) Literal (s). Pour votre Union , vous pouvez définir le discriminateur dans sa valeur: 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]
    """

Syndicats discriminés avec Discriminator appelable

??? API "Documentation API" pydantic.types.Discriminator

Dans le cas d'une Union comportant plusieurs modèles, il n'existe parfois pas un seul champ uniforme pour tous les modèles que vous puissiez utiliser comme discriminateur. C'est le cas d'utilisation parfait pour un Discriminator appelable.

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

Les Discriminator peuvent également être utilisés pour valider les types Union avec des combinaisons de modèles et de types primitifs.

Par exemple:

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. Notez que la fonction de discriminateur appelable renvoie None si une valeur de discriminateur n'est pas trouvée. Lorsque None est renvoyé, cette erreur union_tag_not_found est générée.

!!! note L'utilisation de la syntaxe des champs typing.Annotated peut être pratique pour regrouper les informations Union et discriminator . Voir l'exemple suivant pour plus de détails.

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

!!! avertissement Les syndicats discriminés ne peuvent pas être utilisés avec une seule variante, telle que 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`.

Des syndicats discriminés imbriqués

Un seul discriminateur peut être défini pour un champ, mais vous souhaitez parfois combiner plusieurs discriminateurs. Vous pouvez le faire en créant des types Annotated imbriqués, par exemple:

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

!!! astuce Si vous souhaitez valider des données par rapport à une union, et uniquement une union, vous pouvez utiliser la construction TypeAdapter de pydantic au lieu d'hériter du BaseModel standard.

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

Erreurs de validation syndicale

Lorsque la validation Union échoue, les messages d'erreur peuvent être assez verbeux, car ils produiront des erreurs de validation pour chaque cas de l'union. Ceci est particulièrement visible lorsqu’il s’agit de modèles récursifs, où des raisons peuvent être générées à chaque niveau de récursivité. Les unions discriminées aident à simplifier les messages d'erreur dans ce cas, car les erreurs de validation ne sont produites que pour le cas avec une valeur discriminante correspondante.

Vous pouvez également personnaliser le type d'erreur, le message et le contexte d'un Discriminator en transmettant ces spécifications en tant que paramètres au constructeur Discriminator , comme le montre l'exemple ci-dessous.

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 est l'attribut de type de ValidationError déclenché lorsque la validation échoue.
  2. custom_error_message est l'attribut msg de ValidationError déclenché lorsque la validation échoue.
  3. custom_error_context est l'attribut ctx de ValidationError déclenché lorsque la validation échoue.

Vous pouvez également simplifier les messages d'erreur en étiquetant chaque cas avec un Tag. Ceci est particulièrement utile lorsque vous avez des types complexes comme ceux de cet exemple:

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

本文总阅读量