from typing import Annotatedfrom pydantic import AfterValidator, BaseModel, ValidationErrordef is_even(value: int) -> int: if value % 2 == 1: raise ValueError(f'{value} is not an even number') return valueclass Model(BaseModel): number: Annotated[int, AfterValidator(is_even)]print(Model(number=2))# Output: number=2try: Model(number=1)except ValidationError as e: print(e) # 1 validation error for Model # number # Value error, 1 is not an even number
from pydantic import BaseModel, ValidationError, field_validatorclass Model(BaseModel): number: int @field_validator('number') @classmethod def is_even(cls, value: int) -> int: if value % 2 == 1: raise ValueError(f'{value} is not an even number') return valueprint(Model(number=4))# Output: number=4
When to use After Validators: Use after validators when you need to validate data that’s already been type-checked. They’re the safest and most type-aware option.
Transform or pre-process raw input before type validation:
from typing import Annotated, Anyfrom pydantic import BaseModel, BeforeValidator, ValidationErrordef ensure_list(value: Any) -> Any: """Convert single values to a list.""" if not isinstance(value, list): return [value] return valueclass Model(BaseModel): numbers: Annotated[list[int], BeforeValidator(ensure_list)]print(Model(numbers=2))# Output: numbers=[2]print(Model(numbers=[1, 2, 3]))# Output: numbers=[1, 2, 3]try: Model(numbers='invalid')except ValidationError as e: print(e) # 1 validation error for Model # numbers.0 # Input should be a valid integer
from typing import Annotated, Anyfrom pydantic import BaseModel, PlainValidatordef custom_int_validator(value: Any) -> Any: """Custom validation that accepts 'zero' as 0.""" if value == 'zero': return 0 if isinstance(value, int): return value * 2 return value # Pass through other typesclass Model(BaseModel): number: Annotated[int, PlainValidator(custom_int_validator)]print(Model(number=4))# Output: number=8print(Model(number='zero'))# Output: number=0# Warning: No type checking!print(Model(number='invalid'))# Output: number='invalid'
Use with caution: Plain validators bypass all of Pydantic’s type checking. You’re responsible for ensuring type safety.
Most flexible - control when and how Pydantic’s validation runs:
from typing import Any, Annotatedfrom datetime import datefrom pydantic import BaseModel, ValidationError, ValidationInfo, ValidatorFunctionWrapHandler, WrapValidatordef sixties_validator( val: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo) -> date: # Special case before validation if val == 'epoch': return date.fromtimestamp(0) # Run Pydantic's validation validated = handler(val) # Check result after validation if not (date.fromisoformat('1960-01-01') <= validated < date.fromisoformat('1970-01-01')): raise ValueError(f'{val} is not in the sixties!') return validatedSixtiesDate = Annotated[date, WrapValidator(sixties_validator)]class Model(BaseModel): birth_date: SixtiesDateprint(Model(birth_date='epoch'))# Output: birth_date=datetime.date(1970, 1, 1)print(Model(birth_date='1965-06-15'))# Output: birth_date=datetime.date(1965, 6, 15)try: Model(birth_date='1980-01-01')except ValidationError as e: print(e) # Value error, 1980-01-01 is not in the sixties!
from pydantic import BaseModel, ValidationInfo, field_validatorclass Model(BaseModel): name: str threshold: int @field_validator('threshold') @classmethod def check_threshold(cls, v: int, info: ValidationInfo) -> int: # Access field name print(f"Validating field: {info.field_name}") # Access config if info.config: print(f"Model title: {info.config.get('title')}") # Access other field values (in model validators) # info.data contains validated values so far return vm = Model(name='test', threshold=10)
from typing import Optionalfrom pydantic import BaseModel, field_validatorclass ConditionalModel(BaseModel): require_validation: bool = False value: Optional[int] = None @field_validator('value') @classmethod def validate_value(cls, v: Optional[int], info: ValidationInfo) -> Optional[int]: data = info.data if data.get('require_validation') and v is None: raise ValueError('value is required when require_validation is True') return v# This worksm1 = ConditionalModel(require_validation=False, value=None)# This also worksm2 = ConditionalModel(require_validation=True, value=42)