Vereinigungstypen
Gewerkschaften unterscheiden sich grundlegend von allen anderen von Pydantic validierten Typen: Anstatt zu verlangen, dass alle Felder/Elemente/Werte gültig sind, erfordern Gewerkschaften, dass nur ein Mitglied gültig ist.
Dies führt zu einigen Nuancen bei der Validierung von Gewerkschaften:
- Gegen welche Mitglieder der Gewerkschaft sollten Sie die Daten validieren und in welcher Reihenfolge?
- Welche Fehler werden ausgegeben, wenn die Validierung fehlschlägt?
Die Validierung von Gewerkschaften fühlt sich an, als würde man dem Validierungsprozess eine weitere orthogonale Dimension hinzufügen.
Um diese Probleme zu lösen, unterstützt Pydantic drei grundlegende Ansätze zur Validierung von Gewerkschaften:
- Links-nach-rechts-Modus – der einfachste Ansatz, jedes Mitglied der Gewerkschaft wird der Reihe nach ausprobiert und die erste Übereinstimmung wird zurückgegeben
- Smart-Modus – ähnlich dem „Links-nach-rechts-Modus“, werden die Mitglieder der Reihe nach ausprobiert; Allerdings wird die Validierung über die erste Übereinstimmung hinaus fortgesetzt, um zu versuchen, eine bessere Übereinstimmung zu finden. Dies ist der Standardmodus für die meisten Union-Validierungen
- diskriminierte Gewerkschaften – nur ein Mitglied der Gewerkschaft wird aufgrund eines Diskriminierenden vor Gericht gestellt
Tipp
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¶
Von links nach rechts Modus¶
!!! Hinweis Da dieser Modus oft zu unerwarteten Validierungsergebnissen führt, ist er nicht die Standardeinstellung in Pydantic >=2, sondern union_mode='smart'
.
Bei diesem Ansatz wird die Validierung für jedes Mitglied der Union in der Reihenfolge ihrer Definition versucht, und die erste erfolgreiche Validierung wird als Eingabe akzeptiert.
Wenn die Validierung bei allen Mitgliedern fehlschlägt, umfasst der Validierungsfehler die Fehler aller Mitglieder der Union.
union_mode='left_to_right'
muss als Field
für Union-Felder festgelegt werden, in denen Sie ihn verwenden möchten.
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]
"""
Die Reihenfolge der Mitglieder ist in diesem Fall sehr wichtig, wie das obige Beispiel zeigt:
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
- Wie erwartet wird die Eingabe anhand des
int
Mitglieds validiert und das Ergebnis ist wie erwartet. - Wir befinden uns im laxen Modus und die numerische Zeichenfolge
'123'
ist als Eingabe für das erste Mitglied der Union,int
, gültig. Da dies zuerst versucht wird, erhalten wir das überraschende Ergebnis, dassid
einint
statt einstr
ist.
Smart-Modus¶
Aufgrund der möglicherweise überraschenden Ergebnisse von union_mode='left_to_right'
ist in Pydantic >=2 der Standardmodus für Union
Validierung union_mode='smart'
.
In diesem Modus versucht Pydantic, die beste Übereinstimmung für die Eingabe der Gewerkschaftsmitglieder auszuwählen. Der genaue Algorithmus kann sich zwischen Pydantic-Nebenversionen ändern, um Verbesserungen sowohl bei der Leistung als auch bei der Genauigkeit zu ermöglichen.
Notiz
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 „Smart Mode Algorithmus“
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
!!! Tipp Der Typ Optional[x]
ist eine Abkürzung für Union[x, None]
.
See more details in [Required fields](../concepts/models.md#required-fields).
Diskriminierte Gewerkschaften¶
Diskriminierte Gewerkschaften werden manchmal als „Tagged Unions“ bezeichnet.
Wir können diskriminierte Unions verwenden, um Union
effizienter zu validieren, indem wir auswählen, gegen welches Mitglied der Union validiert werden soll.
Dies macht die Validierung effizienter und vermeidet außerdem eine Fehlerhäufigkeit, wenn die Validierung fehlschlägt.
Das Hinzufügen eines Diskriminators zu Unions bedeutet auch, dass das generierte JSON-Schema die zugehörige OpenAPI-Spezifikation implementiert.
Diskriminierte Gewerkschaften mit str
Diskriminatoren¶
Im Falle einer Union
mit mehreren Modellen gibt es häufig ein gemeinsames Feld für alle Mitglieder der Union, das verwendet werden kann, um zu unterscheiden, gegen welchen Unionsfall die Daten validiert werden sollen; Dies wird in OpenAPI als „Diskriminator“ bezeichnet.
Um Modelle basierend auf diesen Informationen zu validieren, können Sie dasselbe Feld – nennen wir es my_discriminator
– in jedem der Modelle mit einem diskriminierten Wert festlegen, bei dem es sich um einen (oder mehrere) Literal
(e) handelt. Für Ihre Union
können Sie den Diskriminator in seinem Wert festlegen: 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]
"""
Diskriminierte Gewerkschaften mit abrufbarem Discriminator
¶
??? API „API-Dokumentation“ pydantic.types.Discriminator
Bei einer Union
mit mehreren Modellen gibt es manchmal kein einheitliches Feld für alle Modelle, das Sie als Diskriminator verwenden können. Dies ist der perfekte Anwendungsfall für einen aufrufbaren 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
können auch zur Validierung Union
-Typen mit Kombinationen aus Modellen und primitiven Typen verwendet werden.
Zum Beispiel:
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]
"""
- Beachten Sie, dass die aufrufbare Diskriminatorfunktion
None
zurückgibt, wenn kein Diskriminatorwert gefunden wird. WennNone
zurückgegeben wird, wird der Fehlerunion_tag_not_found
ausgelöst.
!!! Hinweis: Die Verwendung der Feldsyntax typing.Annotated
kann hilfreich sein, um die Union
und discriminator
neu zu gruppieren. Weitere Einzelheiten finden Sie im nächsten Beispiel.
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(...))]
```
!!! Warnung Diskriminierte Gewerkschaften können nicht mit nur einer einzigen Variante verwendet werden, wie z. B. 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`.
Verschachtelte diskriminierte Gewerkschaften¶
Für ein Feld kann nur ein Diskriminator festgelegt werden, aber manchmal möchten Sie mehrere Diskriminatoren kombinieren. Sie können dies tun, indem Sie verschachtelte Annotated
-Typen erstellen, z. B.:
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]
"""
!!! Tipp Wenn Sie Daten anhand einer Union, und zwar ausschließlich einer Union, validieren möchten, können Sie TypeAdapter
Konstrukt von pydantic verwenden, anstatt vom Standard BaseModel
zu erben.
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-Validierungsfehler¶
Wenn die Union
-Validierung fehlschlägt, können Fehlermeldungen sehr ausführlich sein, da sie für jeden Fall in der Union zu Validierungsfehlern führen. Dies macht sich besonders beim Umgang mit rekursiven Modellen bemerkbar, bei denen auf jeder Rekursionsebene Gründe generiert werden können. Diskriminierte Vereinigungen tragen in diesem Fall dazu bei, Fehlermeldungen zu vereinfachen, da Validierungsfehler nur für den Fall mit einem passenden Diskriminatorwert erzeugt werden.
Sie können auch den Fehlertyp, die Meldung und den Kontext für einen Discriminator
anpassen, indem Sie diese Spezifikationen als Parameter an den Discriminator
Konstruktor übergeben, wie im folgenden Beispiel dargestellt.
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
ist dastype
desValidationError
das ausgelöst wird, wenn die Validierung fehlschlägt.custom_error_message
ist dasmsg
-Attribut desValidationError
das ausgelöst wird, wenn die Validierung fehlschlägt.custom_error_context
ist dasctx
-Attribut desValidationError
der ausgelöst wird, wenn die Validierung fehlschlägt.
Sie können Fehlermeldungen auch vereinfachen, indem Sie jeden Fall mit einem Tag
kennzeichnen. Dies ist besonders nützlich, wenn Sie über komplexe Typen wie in diesem Beispiel verfügen:
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]
"""
本文总阅读量次