Skip to main content

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: The CoreConfig applying 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 handler in 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_name argument in with_info_* helper functions is deprecated. Use info.field_name from the ValidationInfo object 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.data is only populated when the validator is part of a model validation process (e.g., inside a TypedDict or Model schema). It will be empty or unavailable in standalone validation.