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:
- 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
- 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.
- 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
- Comme prévu, l'entrée est validée par rapport au membre
int
et le résultat est comme prévu. - 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 deid
étant unint
au lieu d'unstr
.
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:
- 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¶
!!! 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]
"""
- Notez que la fonction de discriminateur appelable renvoie
None
si une valeur de discriminateur n'est pas trouvée. LorsqueNone
est renvoyé, cette erreurunion_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'}}}
custom_error_type
est l'attribut detype
deValidationError
déclenché lorsque la validation échoue.custom_error_message
est l'attributmsg
deValidationError
déclenché lorsque la validation échoue.custom_error_context
est l'attributctx
deValidationError
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]
"""
本文总阅读量次