Custom Validation Functions
Custom validation functions allow you to inject arbitrary Python logic into the validation process. In pydantic-core, these are implemented using several validator schemas that can run before, after, or instead of standard validation, or even wrap it entirely.
Applying a Before Validator
A "before" validator runs before the inner schema's validation. It is typically used to normalize or coerce input data.
from pydantic_core import SchemaValidator, core_schema
def normalize_string(v: Any) -> str:
if isinstance(v, bytes):
return v.decode()
return str(v)
# Use no_info_before_validator_function for simple transformations
schema = core_schema.no_info_before_validator_function(
function=normalize_string,
schema=core_schema.str_schema()
)
v = SchemaValidator(schema)
assert v.validate_python(b'hello') == 'hello'
assert v.validate_python(123) == '123'
The no_info_before_validator_function helper creates a BeforeValidatorFunctionSchema where the function receives only the input value.
Applying an After Validator
An "after" validator runs after the inner schema has successfully validated the data. This is ideal for complex business logic or cross-field validation where you can assume the data is already of the correct type.
from pydantic_core import SchemaValidator, core_schema
def check_even(v: int) -> int:
if v % 2 != 0:
raise ValueError('Value must be even')
return v
schema = core_schema.no_info_after_validator_function(
function=check_even,
schema=core_schema.int_schema()
)
v = SchemaValidator(schema)
assert v.validate_python(4) == 4
# v.validate_python(3) # Raises ValidationError: Value must be even
This uses AfterValidatorFunctionSchema via the no_info_after_validator_function helper.
Using Plain Validators
A "plain" validator takes full responsibility for validation. It does not wrap an inner schema.
from pydantic_core import SchemaValidator, core_schema
def custom_logic(v: Any) -> str:
if v == 'secret':
return 'hidden'
return 'public'
schema = core_schema.no_info_plain_validator_function(function=custom_logic)
v = SchemaValidator(schema)
assert v.validate_python('secret') == 'hidden'
assert v.validate_python('other') == 'public'
Plain validators are defined using PlainValidatorFunctionSchema.
Using Wrap Validators
A "wrap" validator provides the most control by receiving a handler to call the inner validation. This allows you to perform logic both before and after the inner validation, or even skip it entirely.
from pydantic_core import SchemaValidator, core_schema
def wrap_validator(
input_value: Any,
handler: core_schema.ValidatorFunctionWrapHandler
) -> str:
# Logic before inner validation
if input_value == 'skip':
return 'skipped'
# Call inner validation
result = handler(input_value)
# Logic after inner validation
return f'Validated: {result}'
schema = core_schema.no_info_wrap_validator_function(
function=wrap_validator,
schema=core_schema.str_schema()
)
v = SchemaValidator(schema)
assert v.validate_python('skip') == 'skipped'
assert v.validate_python('hello') == 'Validated: hello'
Wrap validators use WrapValidatorFunctionSchema and receive a ValidatorFunctionWrapHandler as the second argument.
Accessing Validation Metadata
If your validator needs access to the validation context, configuration, or the data being validated (for model fields), use the with_info variants. These functions receive a ValidationInfo object.
from pydantic_core import SchemaValidator, core_schema
def validator_with_info(v: str, info: core_schema.ValidationInfo) -> str:
# info.context provides access to the validation context
# info.mode indicates if we are in 'python' or 'json' mode
# info.data provides access to other fields (only for model fields)
return f'{v} | context: {info.context}'
schema = core_schema.with_info_before_validator_function(
function=validator_with_info,
schema=core_schema.str_schema()
)
v = SchemaValidator(schema)
# Pass context to validate_python
assert v.validate_python('hello', context='my_context') == 'hello | context: my_context'
The ValidationInfo protocol provides:
context: The current validation context.config: TheCoreConfigapplying to this validation.mode: Either'python'or'json'.data: A dictionary of data being validated (available when validating model fields).field_name: The name of the current field being validated.
Customizing Error Messages
You can wrap any schema in a CustomErrorSchema to override the error raised when validation fails.
from pydantic_core import SchemaValidator, core_schema
schema = core_schema.custom_error_schema(
schema=core_schema.int_schema(),
custom_error_type='not_an_integer',
custom_error_message='Please provide a valid whole number.',
)
v = SchemaValidator(schema)
# v.validate_python('abc') # Raises ValidationError with type 'not_an_integer'
Troubleshooting and Best Practices
- Wrap Validator Handler: Always remember to call the
handlerin a wrap validator if you want the inner schema to execute. If you don't call it, the inner validation is skipped. - Field Name Deprecation: The
field_nameargument inwith_info_*helper functions is deprecated. Useinfo.field_namefrom theValidationInfoobject instead. - Plain Validator Input: Since plain validators have no inner schema, they receive the raw input value. You are responsible for all type checking and coercion.
- ValidationInfo.data availability:
info.datais only populated when the validator is part of a model validation process (e.g., inside aTypedDictorModelschema). It will be empty or unavailable in standalone validation.