Zum Inhalt

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'}]
    """
  1. Die handler -Funktion rufen wir auf, um die Eingabe mit der standardmäßigen pydantic Validierung zu validieren
  2. Wir rufen die handler -Funktion auf, um die Eingabe mit der standardmäßigen pydantic 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 .


本文总阅读量