Vakbonden zijn fundamenteel anders dan alle andere typen Pydantic-validaties - in plaats van te vereisen dat alle velden/items/waarden geldig zijn, vereisen vakbonden dat slechts één lid geldig is.
Dit leidt tot enige nuance rond de manier waarop vakbonden moeten worden gevalideerd:
- Tegen welke leden van de vakbond moet je gegevens valideren, en in welke volgorde?
- welke fouten moeten optreden als de validatie mislukt?
Het valideren van vakbonden voelt als het toevoegen van een nieuwe orthogonale dimensie aan het validatieproces.
Om deze problemen op te lossen ondersteunt Pydantic drie fundamentele benaderingen voor het valideren van vakbonden:
- van links naar rechts-modus - de eenvoudigste aanpak, elk lid van de vakbond wordt op volgorde berecht en de eerste wedstrijd wordt geretourneerd
- slimme modus - vergelijkbaar met de "links naar rechts-modus" leden worden in volgorde geprobeerd; de validatie gaat echter verder dan de eerste match om te proberen een betere match te vinden. Dit is de standaardmodus voor de meeste vakbondsvalidatie
- gediscrimineerde vakbonden - slechts één lid van de vakbond wordt berecht op basis van een discriminator
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.
Union-modi¶
Links naar rechts-modus¶
!!! opmerking Omdat deze modus vaak tot onverwachte validatieresultaten leidt, is dit niet de standaard in Pydantic >=2, maar is union_mode='smart'
de standaard.
Met deze aanpak wordt geprobeerd om elk lid van de vakbond te valideren in de volgorde waarin ze zijn gedefinieerd, en wordt de eerste succesvolle validatie als invoer geaccepteerd.
Als de validatie voor alle leden mislukt, omvat de validatiefout de fouten van alle leden van de vakbond.
union_mode='left_to_right'
moet worden ingesteld als een Field
op samenvoegvelden waar u deze wilt gebruiken.
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]
"""
De volgorde van de leden is in dit geval erg belangrijk, zoals blijkt uit het bovenstaande voorbeeld:
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
- Zoals verwacht wordt de invoer gevalideerd tegen het
int
lid en het resultaat is zoals verwacht. - We bevinden ons in de lakse modus en de numerieke reeks
'123'
is geldig als invoer voor het eerste lid van de vakbond,int
. Omdat dat eerst wordt geprobeerd, krijgen we het verrassende resultaat datid
eenint
is in plaats van eenstr
.
Slimme modus¶
Vanwege de potentieel verrassende resultaten van union_mode='left_to_right'
is in Pydantic >=2 de standaardmodus voor Union
validatie union_mode='smart'
.
In deze modus probeert pydantic de beste match te selecteren voor de inbreng van de vakbondsleden. Het exacte algoritme kan veranderen tussen kleine releases van Pydantic om verbeteringen in zowel de prestaties als de nauwkeurigheid mogelijk te maken.
Opmerking
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.
Slimme modus-algoritme
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¶
!!! tip Het type Optional[x]
is een afkorting voor Union[x, None]
.
See more details in [Required fields](../concepts/models.md#required-fields).
Gediscrimineerde vakbonden¶
Gediscrimineerde vakbonden worden soms "Tagged Unions" genoemd.
We kunnen gediscrimineerde vakbonden gebruiken om Union
efficiënter te valideren, door te kiezen tegen welk lid van de vakbond we valideren.
Dit maakt de validatie efficiënter en voorkomt ook een wildgroei aan fouten wanneer de validatie mislukt.
Het toevoegen van discriminator aan vakbonden betekent ook dat het gegenereerde JSON-schema de bijbehorende OpenAPI-specificatie implementeert.
Gediscrimineerde vakbonden met str
discriminatoren¶
In het geval van een Union
met meerdere modellen is er vaak een gemeenschappelijk veld voor alle leden van de vakbond dat kan worden gebruikt om te onderscheiden op basis van welk vakbondsgeval de gegevens moeten worden gevalideerd; dit wordt in OpenAPI de "discriminator" genoemd.
Om modellen op basis van die informatie te valideren, kunt u hetzelfde veld - laten we het my_discriminator
noemen - in elk van de modellen instellen met een gediscrimineerde waarde, namelijk één (of meerdere) Literal
waarde(n). Voor jouw Union
kun je de discriminator in zijn waarde instellen: 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]
"""
Gediscrimineerde vakbonden met opvraagbare Discriminator
¶
??? api "API-documentatie" pydantic.types.Discriminator
Bij een Union
met meerdere modellen is er soms niet één uniform veld over alle modellen heen dat je als discriminator kunt gebruiken. Dit is de perfecte use case voor een opvraagbare 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
kunnen ook worden gebruikt om Union
-typen te valideren met combinaties van modellen en primitieve typen.
Bijvoorbeeld:
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]
"""
- Merk op dat de opvraagbare discriminatorfunctie
None
retourneert als er geen discriminatorwaarde wordt gevonden. WanneerNone
wordt geretourneerd, treedt dezeunion_tag_not_found
-fout op.
!!! opmerking Het gebruik van de syntaxis van de velden typing.Annotated
kan handig zijn om de Union
en discriminator
te hergroeperen. Zie het volgende voorbeeld voor meer details.
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(...))]
```
!!! waarschuwing Gediscrimineerde vakbonden kunnen niet worden gebruikt met slechts één variant, zoals 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`.
Geneste gediscrimineerde vakbonden¶
Er kan slechts één discriminator voor een veld worden ingesteld, maar soms wilt u meerdere discriminators combineren. U kunt dit doen door geneste Annotated
typen te maken, bijvoorbeeld:
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 Als u gegevens wilt valideren op basis van een unie, en uitsluitend een unie, kunt u TypeAdapter
constructie van pydantic gebruiken in plaats van te erven van het standaard BaseModel
.
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-validatiefouten¶
Wanneer Union
validatie mislukt, kunnen foutmeldingen behoorlijk uitgebreid zijn, omdat ze voor elk geval in de Union validatiefouten zullen opleveren. Dit is vooral merkbaar bij het omgaan met recursieve modellen, waarbij redenen op elk recursieniveau kunnen worden gegenereerd. Gediscrimineerde vakbonden helpen in dit geval foutmeldingen te vereenvoudigen, omdat validatiefouten alleen worden geproduceerd voor het geval met een overeenkomende discriminatorwaarde.
U kunt ook het fouttype, het bericht en de context voor een Discriminator
aanpassen door deze specificaties als parameters door te geven aan de Discriminator
constructor, zoals u kunt zien in het onderstaande voorbeeld.
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
is hettype
van deValidationError
die wordt gegenereerd wanneer de validatie mislukt.custom_error_message
is hetmsg
kenmerk van deValidationError
die wordt gegenereerd wanneer de validatie mislukt.custom_error_context
is hetctx
-kenmerk van deValidationError
die wordt gegenereerd wanneer de validatie mislukt.
U kunt foutmeldingen ook vereenvoudigen door elke case te labelen met een Tag
. Dit is vooral handig als u complexe typen heeft, zoals die in dit voorbeeld:
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]
"""
本文总阅读量次