Validatoren
!!! Warnung „🚧 Work in Progress“ Diese Seite ist in Arbeit.
Diese Seite bietet Beispielausschnitte zum Erstellen komplexerer, benutzerdefinierter Validatoren in Pydantic.
Verwenden benutzerdefinierter Validatoren mit Annotated
-Metadaten¶
In diesem Beispiel erstellen wir einen benutzerdefinierten Validator, der an einen Annotated
-Typ angehängt ist und sicherstellt, dass ein datetime
-Objekt eine bestimmte Zeitzonenbeschränkung einhält.
Der benutzerdefinierte Validator unterstützt die Zeichenfolgenspezifikation der Zeitzone und löst einen Fehler aus, wenn das Objekt datetime
nicht über die richtige Zeitzone verfügt.
Wir verwenden __get_pydantic_core_schema__
im Validator, um das Schema des annotierten Typs anzupassen (in diesem Fall datetime
), was uns das Hinzufügen benutzerdefinierter Validierungslogik ermöglicht. Insbesondere verwenden wir eine wrap
Validator-Funktion, damit wir Vorgänge sowohl vor als auch nach der standardmäßigen pydantic
Validierung eines datetime
ausführen können.
import datetime as dt
from dataclasses import dataclass
from pprint import pprint
from typing import Any, Callable, Optional
import pytz
from pydantic_core import CoreSchema, core_schema
from typing_extensions import Annotated
from pydantic import (
GetCoreSchemaHandler,
PydanticUserError,
TypeAdapter,
ValidationError,
)
@dataclass(frozen=True)
class MyDatetimeValidator:
tz_constraint: Optional[str] = None
def tz_constraint_validator(
self,
value: dt.datetime,
handler: Callable, # (1)!
):
"""Validate tz_constraint and tz_info."""
# handle naive datetimes
if self.tz_constraint is None:
assert (
value.tzinfo is None
), 'tz_constraint is None, but provided value is tz-aware.'
return handler(value)
# validate tz_constraint and tz-aware tzinfo
if self.tz_constraint not in pytz.all_timezones:
raise PydanticUserError(
f'Invalid tz_constraint: {self.tz_constraint}',
code='unevaluable-type-annotation',
)
result = handler(value) # (2)!
assert self.tz_constraint == str(
result.tzinfo
), f'Invalid tzinfo: {str(result.tzinfo)}, expected: {self.tz_constraint}'
return result
def __get_pydantic_core_schema__(
self,
source_type: Any,
handler: GetCoreSchemaHandler,
) -> CoreSchema:
return core_schema.no_info_wrap_validator_function(
self.tz_constraint_validator,
handler(source_type),
)
LA = 'America/Los_Angeles'
ta = TypeAdapter(Annotated[dt.datetime, MyDatetimeValidator(LA)])
print(
ta.validate_python(dt.datetime(2023, 1, 1, 0, 0, tzinfo=pytz.timezone(LA)))
)
#> 2023-01-01 00:00:00-07:53
LONDON = 'Europe/London'
try:
ta.validate_python(
dt.datetime(2023, 1, 1, 0, 0, tzinfo=pytz.timezone(LONDON))
)
except ValidationError as ve:
pprint(ve.errors(), width=100)
"""
[{'ctx': {'error': AssertionError('Invalid tzinfo: Europe/London, expected: America/Los_Angeles')},
'input': datetime.datetime(2023, 1, 1, 0, 0, tzinfo=<DstTzInfo 'Europe/London' LMT-1 day, 23:59:00 STD>),
'loc': (),
'msg': 'Assertion failed, Invalid tzinfo: Europe/London, expected: America/Los_Angeles',
'type': 'assertion_error',
'url': 'https://errors.pydantic.dev/2.8/v/assertion_error'}]
"""
- Die
handler
-Funktion rufen wir auf, um die Eingabe mit der standardmäßigenpydantic
Validierung zu validieren - Wir rufen die
handler
-Funktion auf, um die Eingabe mit der standardmäßigenpydantic
Validierung in diesem Wrap-Validator zu validieren
Auf ähnliche Weise können wir auch UTC-Offset-Beschränkungen erzwingen. Angenommen, wir haben eine lower_bound
und eine upper_bound
, können wir einen benutzerdefinierten Validator erstellen, um sicherzustellen, dass unsere datetime
einen UTC-Offset hat, der innerhalb der von uns definierten Grenze liegt:
import datetime as dt
from dataclasses import dataclass
from pprint import pprint
from typing import Any, Callable
import pytz
from pydantic_core import CoreSchema, core_schema
from typing_extensions import Annotated
from pydantic import GetCoreSchemaHandler, TypeAdapter, ValidationError
@dataclass(frozen=True)
class MyDatetimeValidator:
lower_bound: int
upper_bound: int
def validate_tz_bounds(self, value: dt.datetime, handler: Callable):
"""Validate and test bounds"""
assert value.utcoffset() is not None, 'UTC offset must exist'
assert self.lower_bound <= self.upper_bound, 'Invalid bounds'
result = handler(value)
hours_offset = value.utcoffset().total_seconds() / 3600
assert (
self.lower_bound <= hours_offset <= self.upper_bound
), 'Value out of bounds'
return result
def __get_pydantic_core_schema__(
self,
source_type: Any,
handler: GetCoreSchemaHandler,
) -> CoreSchema:
return core_schema.no_info_wrap_validator_function(
self.validate_tz_bounds,
handler(source_type),
)
LA = 'America/Los_Angeles' # UTC-7 or UTC-8
ta = TypeAdapter(Annotated[dt.datetime, MyDatetimeValidator(-10, -5)])
print(
ta.validate_python(dt.datetime(2023, 1, 1, 0, 0, tzinfo=pytz.timezone(LA)))
)
#> 2023-01-01 00:00:00-07:53
LONDON = 'Europe/London'
try:
print(
ta.validate_python(
dt.datetime(2023, 1, 1, 0, 0, tzinfo=pytz.timezone(LONDON))
)
)
except ValidationError as e:
pprint(e.errors(), width=100)
"""
[{'ctx': {'error': AssertionError('Value out of bounds')},
'input': datetime.datetime(2023, 1, 1, 0, 0, tzinfo=<DstTzInfo 'Europe/London' LMT-1 day, 23:59:00 STD>),
'loc': (),
'msg': 'Assertion failed, Value out of bounds',
'type': 'assertion_error',
'url': 'https://errors.pydantic.dev/2.8/v/assertion_error'}]
"""
Validieren verschachtelter Modellfelder¶
Hier zeigen wir zwei Möglichkeiten zur Validierung eines Felds eines verschachtelten Modells, wobei der Validator Daten aus dem übergeordneten Modell verwendet.
In diesem Beispiel erstellen wir einen Validator, der prüft, ob das Passwort jedes Benutzers nicht in einer Liste verbotener Passwörter enthalten ist, die vom übergeordneten Modell angegeben wird.
Eine Möglichkeit hierfür besteht darin, einen benutzerdefinierten Validator auf dem äußeren Modell zu platzieren:
from typing import List
from typing_extensions import Self
from pydantic import BaseModel, ValidationError, model_validator
class User(BaseModel):
username: str
password: str
class Organization(BaseModel):
forbidden_passwords: List[str]
users: List[User]
@model_validator(mode='after')
def validate_user_passwords(self) -> Self:
"""Check that user password is not in forbidden list. Raise a validation error if a forbidden password is encountered."""
for user in self.users:
current_pw = user.password
if current_pw in self.forbidden_passwords:
raise ValueError(
f'Password {current_pw} is forbidden. Please choose another password for user {user.username}.'
)
return self
data = {
'forbidden_passwords': ['123'],
'users': [
{'username': 'Spartacat', 'password': '123'},
{'username': 'Iceburgh', 'password': '87'},
],
}
try:
org = Organization(**data)
except ValidationError as e:
print(e)
"""
1 validation error for Organization
Value error, Password 123 is forbidden. Please choose another password for user Spartacat. [type=value_error, input_value={'forbidden_passwords': [...gh', 'password': '87'}]}, input_type=dict]
"""
Alternativ kann ein benutzerdefinierter Validator in der verschachtelten Modellklasse ( User
) verwendet werden, wobei die verbotenen Passwortdaten aus dem übergeordneten Modell über den Validierungskontext übergeben werden.
!!! Warnung Die Möglichkeit, den Kontext innerhalb eines Validators zu ändern, verleiht der verschachtelten Validierung viel mehr Leistung, kann aber auch zu verwirrendem oder schwer zu debuggendem Code führen. Nutzen Sie diesen Ansatz auf eigenes Risiko!
from typing import List
from pydantic import BaseModel, ValidationError, ValidationInfo, field_validator
class User(BaseModel):
username: str
password: str
@field_validator('password', mode='after')
@classmethod
def validate_user_passwords(
cls, password: str, info: ValidationInfo
) -> str:
"""Check that user password is not in forbidden list."""
forbidden_passwords = (
info.context.get('forbidden_passwords', []) if info.context else []
)
if password in forbidden_passwords:
raise ValueError(f'Password {password} is forbidden.')
return password
class Organization(BaseModel):
forbidden_passwords: List[str]
users: List[User]
@field_validator('forbidden_passwords', mode='after')
@classmethod
def add_context(cls, v: List[str], info: ValidationInfo) -> List[str]:
if info.context is not None:
info.context.update({'forbidden_passwords': v})
return v
data = {
'forbidden_passwords': ['123'],
'users': [
{'username': 'Spartacat', 'password': '123'},
{'username': 'Iceburgh', 'password': '87'},
],
}
try:
org = Organization.model_validate(data, context={})
except ValidationError as e:
print(e)
"""
1 validation error for Organization
users.0.password
Value error, Password 123 is forbidden. [type=value_error, input_value='123', input_type=str]
"""
Beachten Sie, dass, wenn die Kontexteigenschaft nicht in model_validate
enthalten ist, info.context
None
ist und die Liste der verbotenen Passwörter in der obigen Implementierung nicht zum Kontext hinzugefügt wird. Daher würde validate_user_passwords
nicht die gewünschte Passwortvalidierung durchführen.
Weitere Details zum Validierungskontext finden Sie hier .
本文总阅读量次