Custom datetime validator with timezone constraint via Annotated
A custom datetime validator can be implemented using the `__get_pydantic_core_schema__` method on an Annotated metadata class. The validator can enforce timezone constraints by validating that a datetime object has the correct timezone. Use a wrap validator function with `ValidatorFunctionWrapHandler` to perform operations both before and after default Pydantic validation. The `handler` function is called to validate the input with standard Pydantic validation. Use `core_schema.no_info_wrap_validator_function()` to attach the validator to the schema.
Custom datetime validator with UTC offset bounds
A custom datetime validator can enforce UTC offset constraints by checking that a datetime object's UTC offset falls within defined lower and upper bounds in hours. Implement the validator using `__get_pydantic_core_schema__` with a wrap validator that calls `handler(value)` to validate with standard Pydantic validation, then asserts the UTC offset is within bounds using `value.utcoffset().total_seconds() / 3600` to convert to hours.
Validating nested model fields with model_validator
Nested model validation can be performed using a `@model_validator(mode='after')` decorator on the parent model class. This validator receives the fully validated parent instance (via `Self` type hint) and can access nested model fields to perform cross-field validation. Raise `ValueError` with a descriptive message if validation fails. This approach is simpler than passing context to nested validators.
Validating nested model fields with field_validator and validation context
Nested model validation can alternatively be performed using `@field_validator` on the nested model class with access to validation context from the parent. The nested validator receives `info: ValidationInfo` and accesses forbidden data via `info.context.get()`. The parent model must update the context in its own validator to make data available to nested validators. This approach requires passing `context={}` to `model_validate()`, otherwise `info.context` will be `None` and context updates will not occur.
Validation error messages are used by Logfire
The error messages written in custom validator `raise ValueError()` calls are important: they appear in validation error output and are also consumed by external tooling like Logfire to generate explanations of failed validations. Craft error messages carefully as they will be read by users and developers when validation rules reject data.
ValidatorFunctionWrapHandler in wrap validators
The `ValidatorFunctionWrapHandler` type is used as the handler parameter in wrap validator functions. Call `handler(value)` within the wrap validator to perform standard Pydantic validation, allowing custom validation logic to wrap around the default validation.
Context mutation in nested validators can lead to confusing code
While validation context can be mutated within a validator to enable nested validation, this approach adds complexity and can lead to code that is difficult to debug. Use validation context mutation at your own risk.
Pydantic AI validation context and retry behavior
When an LLM returns data that fails Pydantic validation, Pydantic AI automatically feeds the validation errors back to the model and asks it to retry. Validation context passed to the agent is used for validation only and is not sent to the model itself.
Wrap validators for customization
Wrap validators are a powerful way to customize validation in Pydantic V2. They allow you to intercept and modify the validation process on a per-field basis.
Wrap validator example
Example showing wrap validator usage:
from datetime import datetime, timezone
from typing import Any
from pydantic_core.core_schema import ValidatorFunctionWrapHandler
from pydantic import BaseModel, field_validator
class Meeting(BaseModel):
when: datetime
@field_validator('when', mode='wrap')
def when_now(
cls, input_value: Any, handler: ValidatorFunctionWrapHandler
) -> datetime:
if input_value == 'now':
return datetime.now()
when = handler(input_value)
if when.tzinfo is None:
when = when.replace(tzinfo=timezone.utc)
return when
print(Meeting(when='2020-01-01T12:00+01:00'))
#> when=datetime.datetime(2020, 1, 1, 12, 0, tzinfo=TzInfo(3600))
print(Meeting(when='now'))
#> when=datetime.datetime(2032, 1, 2, 3, 4, 5, 6)
print(Meeting(when='2020-01-01T12:00'))
#> when=datetime.datetime(2020, 1, 1, 12, 0, tzinfo=datetime.timezone.utc)
@validator deprecated, use @field_validator
In Pydantic V2, `@validator` is deprecated and should be replaced with `@field_validator`, which provides new features and improvements. The `@validator` decorator no longer supports the `each_item` keyword argument; instead, annotate the type argument using `Annotated`.
@field_validator no longer accepts field and config arguments
In Pydantic V2, `@field_validator` functions can no longer add `field` or `config` arguments to their signature. To access configuration, use `info.config`. To access field information, use `info.field_name` to index into `cls.model_fields`.
always=True in validators applies standard validators to defaults
In Pydantic V2, if you use `always=True` in a validator, standard validators for the annotated type will also be applied to defaults, not just custom validators. Use `validate_default=True` in `Field` instead for similar behavior with more flexibility.
@root_validator deprecated, use @model_validator
In Pydantic V2, `@root_validator` is deprecated and should be replaced with `@model_validator`, which provides new features and improvements. Note that allowed signatures have changed. In some circumstances (like assignment when `validate_assignment` is True), `@model_validator` receives a model instance instead of a dict.
@root_validator requires skip_on_failure=True explicitly
In Pydantic V2, the deprecated `@root_validator` decorator requires `skip_on_failure=True` to be set explicitly due to refactors in validation logic. The default value can no longer be used.
TypeError no longer converted to ValidationError in validators
In Pydantic V2, when a `TypeError` is raised in a validator function, it is no longer converted into a `ValidationError`. It remains a `TypeError`. This applies to all validation decorators.
allow_reuse keyword argument no longer necessary
In Pydantic V2, the `allow_reuse` keyword argument for validators is no longer necessary. The approach to detecting repeatedly defined functions has been overhauled to only error for redefinition within a single class, reducing false positives.
@validate_arguments renamed to @validate_call
In Pydantic V2, the `@validate_arguments` decorator has been renamed to `@validate_call`. The decorated function no longer has attributes like `raw_function` or `validate` methods added to it.
Four types of field validators
Pydantic supports four types of field validators: After validators run after Pydantic's internal validation; Before validators run before Pydantic's internal parsing and validation; Plain validators terminate validation immediately after returning, skipping further validators and Pydantic's internal validation; Wrap validators are the most flexible, allowing code before or after Pydantic processes input, or immediate termination.
After validators defined with AfterValidator or @field_validator
After validators can be defined using the Annotated pattern with AfterValidator(callable) or using the @field_validator('field_name', mode='after') decorator as a classmethod. They run after Pydantic's internal validation and are generally more type safe. The 'after' mode is the default for the decorator and can be omitted. The validator must return the validated value.
Before validators defined with BeforeValidator or @field_validator
Before validators can be defined using the Annotated pattern with BeforeValidator(callable) or using the @field_validator('field_name', mode='before') decorator as a classmethod. They run before Pydantic's internal parsing and validation. Before validators take the raw input which can be any arbitrary object, and should use Any as the type hint. The value returned is then validated against the field's type annotation by Pydantic. Avoid mutating the value directly if raising a validation error later, as the mutated value may be passed to other validators in unions.
Plain validators terminate validation immediately
Plain validators can be defined using the Annotated pattern with PlainValidator(callable) or using the @field_validator('field_name', mode='plain') decorator as a classmethod. They act similarly to before validators but terminate validation immediately after returning, so no further validators are called and Pydantic does not perform any internal validation against the field type. The field will accept any value the validator returns, regardless of the field's type annotation.
Wrap validators with ValidatorFunctionWrapHandler
Wrap validators are the most flexible of all validator types. They must be defined with a mandatory extra 'handler' parameter of type ValidatorFunctionWrapHandler, which is a callable taking the value to be validated. Wrap validators can run code before or after Pydantic and other validators process the input, or terminate validation immediately by returning early or raising an error. The handler delegates validation to Pydantic and can be wrapped in try..except or not called at all. They can be defined using the Annotated pattern with WrapValidator(callable) or using the @field_validator('field_name', mode='wrap') decorator as a classmethod.
ValidatorFunctionWrapHandler import and usage
ValidatorFunctionWrapHandler is imported from pydantic and is used as the type for the handler parameter in wrap validators. It is a callable that takes the value to be validated and delegates validation to Pydantic. You can wrap the handler call in try..except blocks to handle validation errors.
Three types of model validators
Pydantic supports three types of model validators: After validators run after the whole model has been validated and are defined as instance methods; Before validators run before the model is instantiated and are defined as classmethods; Wrap validators are the most flexible and run code before or after Pydantic processes input data.
Model after validators must return Self
Model after validators run after the whole model has been validated and are defined as instance methods. They must return the validated instance (Self). They can be seen as post-initialization hooks.
Model before validators are classmethods taking Any
Model before validators run before the model is instantiated and are defined as classmethods. They take the raw input data (type hinted as Any) which can be any arbitrary object, not necessarily a dictionary. They should avoid mutating the value directly if raising a validation error later, as the mutated value may be passed to other validators.
Model wrap validators take ModelWrapValidatorHandler
Model wrap validators take a handler parameter of type ModelWrapValidatorHandler[Self] and can run code before or after Pydantic processes the input data, or terminate validation immediately. They are defined as classmethods and return Self.
Raising validation errors with ValueError, AssertionError, or PydanticCustomError
Three types of exceptions can be used to raise validation errors inside validators: ValueError is the most common; AssertionError using the assert statement also works but is skipped when Python is run with the -O optimization flag; PydanticCustomError from pydantic_core is more verbose but provides extra flexibility with custom error types and context.
ValidationInfo argument in validators
Both field and model validators (in all modes) can optionally take an extra ValidationInfo argument providing useful extra information including already validated data (via the data property, None for model validators), user-defined context (via the context property), the current validation mode ('python', 'json', or 'strings'), and the current field name for field validators (via the field_name property).
Accessing already validated data with ValidationInfo.data
For field validators, the already validated data can be accessed using the ValidationInfo.data property, which is a dictionary of field names to validated values. The data property is None for model validators. Validation is performed in the order fields are defined, so you must not access a field that hasn't been validated yet.
Passing and accessing validation context
A context object can be passed to validation methods like model_validate(data, context={...}), which can be accessed inside validator functions using the ValidationInfo.context property. It is currently not possible to provide context when directly instantiating a model with Model(...). The context can be a dictionary or any other object.
Validator ordering in annotated pattern
When using the annotated pattern, the order validators are applied is: Before and Wrap validators run from right to left, then After validators run from left to right. Validators defined using the decorator pattern are converted to their annotated form counterpart and added last, so the same ordering logic applies.
Using annotated pattern for reusable validators
One key benefit of using the annotated pattern is to make validators reusable. You can create a type alias like EvenNumber = Annotated[int, AfterValidator(is_even)] and then use it across multiple models or nested in collections like list[EvenNumber]. Validators can also be composed by creating Annotated[EvenNumber, AfterValidator(another_validator)].
Using decorator pattern for validators on multiple fields
One key benefit of using the @field_validator decorator is to apply the validator to multiple fields in a single decorator. Pass multiple field names as arguments: @field_validator('f1', 'f2', mode='before'). To apply a validator to all fields including those in subclasses, pass '*' as the field name. The check_fields argument can be set to False to disable the check that provided field names are defined on the model, useful when the validator is on a base class.
Default values are not validated by default
Default values of fields are not validated unless configured to do so, and thus custom validators will not be applied to them. This is documented in the fields documentation under validate-default-values.
Model validators inherit in subclasses
A model validator defined in a base class will be called during the validation of a subclass instance. Overriding a model validator in a subclass will override the base class' validator, and thus only the subclass' version of the validator will be called.
InstanceOf special type for instance validation
InstanceOf is a special utility that can be used to validate that a value is an instance of a given class. It is imported from pydantic.functional_validators and used like InstanceOf[ClassName]. When validation fails, the error type is 'is_instance_of'.
SkipValidation special type skips field validation
SkipValidation is a special utility that can be used to skip validation on a field. It is used like SkipValidation[FieldType]. When a value has the wrong type, it will emit a warning during serialization.
ValidateAs special type for custom type validation
ValidateAs is a special utility that can be used to validate a custom type from a type natively supported by Pydantic. It takes two arguments: a validator model (like a BaseModel or TypeAdapter target) and a conversion function that transforms the validated result into the custom type. It is particularly useful when using custom types with multiple fields.
PydanticUseDefault signals to use field default
PydanticUseDefault is imported from pydantic_core and can be raised inside validators to notify Pydantic that the default value should be used instead of the provided value. It is typically used in BeforeValidator to replace None or other sentinel values with the field default.
Field validator example: validating even numbers
Example of an after validator using the decorator pattern:
from pydantic import BaseModel, ValidationError, field_validator
class Model(BaseModel):
number: int
@field_validator('number', mode='after')
@classmethod
def is_even(cls, value: int) -> int:
if value % 2 == 1:
raise ValueError(f'{value} is not an even number')
return value
Model(number=1) raises ValidationError with error type 'value_error'
Before validator example: ensure list
Example of a before validator using the decorator pattern:
from typing import Any
from pydantic import BaseModel, ValidationError, field_validator
class Model(BaseModel):
numbers: list[int]
@field_validator('numbers', mode='before')
@classmethod
def ensure_list(cls, value: Any) -> Any:
if not isinstance(value, list):
return [value]
else:
return value
Model(numbers=2) produces numbers=[2]; Model(numbers='str') raises ValidationError
Plain validator example: double or pass through
Example of a plain validator using the decorator pattern:
from typing import Any
from pydantic import BaseModel, field_validator
class Model(BaseModel):
number: int
@field_validator('number', mode='plain')
@classmethod
def val_number(cls, value: Any) -> Any:
if isinstance(value, int):
return value * 2
else:
return value
Model(number=4) produces number=8; Model(number='invalid') produces number='invalid' despite str not matching int type
Wrap validator example: truncate string on error
Example of a wrap validator using the decorator pattern:
from typing import Any, Annotated
from pydantic import BaseModel, Field, ValidationError, ValidatorFunctionWrapHandler, field_validator
class Model(BaseModel):
my_string: Annotated[str, Field(max_length=5)]
@field_validator('my_string', mode='wrap')
@classmethod
def truncate(cls, value: Any, handler: ValidatorFunctionWrapHandler) -> str:
try:
return handler(value)
except ValidationError as err:
if err.errors()[0]['type'] == 'string_too_long':
return handler(value[:5])
else:
raise
Model(my_string='abcde') produces my_string='abcde'; Model(my_string='abcdef') produces my_string='abcde'
Model after validator example: check passwords match
Example of a model after validator:
from typing_extensions import Self
from pydantic import BaseModel, model_validator
class UserModel(BaseModel):
username: str
password: str
password_repeat: str
@model_validator(mode='after')
def check_passwords_match(self) -> Self:
if self.password != self.password_repeat:
raise ValueError('Passwords do not match')
return self
Model before validator example: reject card number
Example of a model before validator:
from typing import Any
from pydantic import BaseModel, model_validator
class UserModel(BaseModel):
username: str
@model_validator(mode='before')
@classmethod
def check_card_number_not_present(cls, data: Any) -> Any:
if isinstance(data, dict):
if 'card_number' in data:
raise ValueError("'card_number' should not be included")
return data
Model wrap validator example: log failed validation
Example of a model wrap validator:
import logging
from typing import Any
from typing_extensions import Self
from pydantic import BaseModel, ModelWrapValidatorHandler, ValidationError, model_validator
class UserModel(BaseModel):
username: str
@model_validator(mode='wrap')
@classmethod
def log_failed_validation(cls, data: Any, handler: ModelWrapValidatorHandler[Self]) -> Self:
try:
return handler(data)
except ValidationError:
logging.error('Model %s failed to validate with data %s', cls, data)
raise
PydanticCustomError example with custom error type
Example of raising PydanticCustomError:
from pydantic_core import PydanticCustomError
from pydantic import BaseModel, ValidationError, field_validator
class Model(BaseModel):
x: int
@field_validator('x', mode='after')
@classmethod
def validate_x(cls, v: int) -> int:
if v % 42 == 0:
raise PydanticCustomError(
'the_answer_error',
'{number} is the answer!',
{'number': v},
)
return v
Model(x=84) raises ValidationError with type='the_answer_error' and message '84 is the answer!'
ValidationInfo.data example for field validators
Example of accessing already validated data with ValidationInfo.data:
from pydantic import BaseModel, ValidationInfo, field_validator
class UserModel(BaseModel):
password: str
password_repeat: str
username: str
@field_validator('password_repeat', mode='after')
@classmethod
def check_passwords_match(cls, value: str, info: ValidationInfo) -> str:
if value != info.data['password']:
raise ValueError('Passwords do not match')
return value
Note: username is not accessible in ValidationInfo.data because it is defined after password_repeat
Validation context example with model_validate
Example of passing and using validation context:
from pydantic import BaseModel, ValidationInfo, field_validator
class Model(BaseModel):
text: str
@field_validator('text', mode='after')
@classmethod
def remove_stopwords(cls, v: str, info: ValidationInfo) -> str:
if isinstance(info.context, dict):
stopwords = info.context.get('stopwords', set())
v = ' '.join(w for w in v.split() if w.lower() not in stopwords)
return v
data = {'text': 'This is an example document'}
Model.model_validate(data)
Model.model_validate(data, context={'stopwords': ['this', 'is', 'an']}) produces text='example document'
InstanceOf example for type validation
Example of using InstanceOf:
from pydantic import BaseModel, InstanceOf, ValidationError
class Fruit:
def __repr__(self):
return self.__class__.__name__
class Banana(Fruit): ...
class Apple(Fruit): ...
class Basket(BaseModel):
fruits: list[InstanceOf[Fruit]]
Basket(fruits=[Banana(), Apple()]) succeeds
Basket(fruits=[Banana(), 'Apple']) raises ValidationError with type='is_instance_of'
SkipValidation example
Example of using SkipValidation:
from pydantic import BaseModel, SkipValidation
class Model(BaseModel):
names: list[SkipValidation[str]]
m = Model(names=['foo', 'bar'])
m = Model(names=['foo', 123])
The validation of the second item (123) is skipped. If it has the wrong type it will emit a warning during serialization.
ValidateAs example for custom type
Example of using ValidateAs:
from typing import Annotated
from pydantic import BaseModel, TypeAdapter, ValidateAs
class MyCls:
def __init__(self, a: int) -> None:
self.a = a
def __repr__(self) -> str:
return f"MyCls(a={self.a})"
class ValModel(BaseModel):
a: int
ta = TypeAdapter(
Annotated[MyCls, ValidateAs(ValModel, lambda v: MyCls(a=v.a))]
)
ta.validate_python({'a': 1}) produces MyCls(a=1)
PydanticUseDefault example
Example of using PydanticUseDefault:
from typing import Annotated, Any
from pydantic_core import PydanticUseDefault
from pydantic import BaseModel, BeforeValidator
def default_if_none(value: Any) -> Any:
if value is None:
raise PydanticUseDefault()
return value
class Model(BaseModel):
name: Annotated[str, BeforeValidator(default_if_none)] = 'default_name'
Model(name=None) produces name='default_name'
Reusable annotated validator type
Example of creating reusable validator types with Annotated:
from typing import Annotated
from pydantic import AfterValidator, BaseModel
def is_even(value: int) -> int:
if value % 2 == 1:
raise ValueError(f'{value} is not an even number')
return value
EvenNumber = Annotated[int, AfterValidator(is_even)]
class Model1(BaseModel):
my_number: EvenNumber
class Model2(BaseModel):
other_number: Annotated[EvenNumber, AfterValidator(lambda v: v + 2)]
class Model3(BaseModel):
list_of_even_numbers: list[EvenNumber]
Decorator pattern for validators on multiple fields
Example of applying a field validator to multiple fields:
from pydantic import BaseModel, field_validator
class Model(BaseModel):
f1: str
f2: str
@field_validator('f1', 'f2', mode='before')
@classmethod
def capitalize(cls, value: str) -> str:
return value.capitalize()
Context variable approach for __init__ with context
Example of providing context when directly instantiating a model using ContextVar:
from __future__ import annotations
from collections.abc import Generator
from contextlib import contextmanager
from contextvars import ContextVar
from typing import Any
from pydantic import BaseModel, ValidationInfo, field_validator
_init_context_var = ContextVar('_init_context_var', default=None)
@contextmanager
def init_context(value: dict[str, Any]) -> Generator[None]:
token = _init_context_var.set(value)
try:
yield
finally:
_init_context_var.reset(token)
class Model(BaseModel):
my_number: int
def __init__(self, /, **data: Any) -> None:
self.__pydantic_validator__.validate_python(
data,
self_instance=self,
context=_init_context_var.get(),
)
@field_validator('my_number')
@classmethod
def multiply_with_context(cls, value: int, info: ValidationInfo) -> int:
if isinstance(info.context, dict):
multiplier = info.context.get('multiplier', 1)
value = value * multiplier
return value
with init_context({'multiplier': 3}):
print(Model(my_number=2))