Skip to main content

Custom Validator Functions

Custom validator functions allow you to extend the core validation logic by injecting Python functions at different stages of the validation process. You can use these to transform input, post-process results, or completely override validation for a specific field.

Implement a Before Validator

Use a "before" validator to sanitize or transform raw input before it is passed to the inner schema for validation.

from pydantic_core import SchemaValidator, core_schema

def sanitize_input(v: bytes) -> str:
# Transform raw bytes to string before core validation
return v.decode().strip() + ' world'

# Define a schema that calls sanitize_input BEFORE the string schema
func_schema = core_schema.no_info_before_validator_function(
function=sanitize_input,
schema=core_schema.str_schema()
)

v = SchemaValidator(func_schema)
assert v.validate_python(b' hello ') == 'hello world'

In this example, no_info_before_validator_function ensures that sanitize_input runs first. The result of this function is then passed to core_schema.str_schema().

Implement an After Validator

Use an "after" validator to perform complex validation or post-processing on a value that has already been validated by the inner schema.

from pydantic_core import SchemaValidator, core_schema

def append_suffix(v: str) -> str:
# v is already a validated string here
return v + '!!!'

# Define a schema that calls append_suffix AFTER the string schema
func_schema = core_schema.no_info_after_validator_function(
function=append_suffix,
schema=core_schema.str_schema()
)

v = SchemaValidator(func_schema)
assert v.validate_python('hello') == 'hello!!!'

Control Validation with Wrap Validators

Wrap validators provide the most control by passing a handler (of type ValidatorFunctionWrapHandler) to your function. This allows you to trigger the inner validation at any point, catch its errors, or skip it entirely.

from pydantic_core import SchemaValidator, core_schema

def wrap_validator(
v: Any,
handler: core_schema.ValidatorFunctionWrapHandler,
) -> str:
try:
# Call the inner schema validation
result = handler(input_value=v)
return f"Success: {result}"
except Exception:
# Fallback or custom error handling
return "Validation failed, but I caught it"

schema = core_schema.no_info_wrap_validator_function(
function=wrap_validator,
schema=core_schema.str_schema()
)

v = SchemaValidator(schema)
assert v.validate_python('hello') == 'Success: hello'
assert v.validate_python(123) == 'Validation failed, but I caught it'

The handler can also accept an outer_location to customize the error path if validation fails: handler(input_value=v, outer_location='my_custom_loc').

Access Validation Metadata

If your validator needs access to the validation context, configuration, or other field data, use the with_info_* variants. These pass a ValidationInfo object as the last argument.

from pydantic_core import SchemaValidator, core_schema

def check_with_context(v: int, info: core_schema.ValidationInfo) -> int:
# Access context passed during validate_python
min_val = info.context.get('min_val', 0)
if v < min_val:
raise ValueError(f"Value must be at least {min_val}")
return v

schema = core_schema.with_info_plain_validator_function(function=check_with_context)
v = SchemaValidator(schema)

# Pass context to the validator
assert v.validate_python(10, context={'min_val': 5}) == 10

The ValidationInfo object provides:

  • context: The context dictionary passed to the validator.
  • config: The CoreConfig applied to the validation.
  • mode: Either 'python' or 'json'.
  • data: A dictionary of other fields being validated (available when used within a model/TypedDict).
  • field_name: The name of the field being validated.

Replace Validation Entirely

Use a "plain" validator when you want to bypass core validation logic and handle everything in your Python function.

from pydantic_core import SchemaValidator, core_schema

def custom_logic(v: Any) -> str:
if not isinstance(v, str):
raise ValueError("Expected a string")
return v.upper()

schema = core_schema.no_info_plain_validator_function(function=custom_logic)
v = SchemaValidator(schema)

assert v.validate_python('hello') == 'HELLO'

Troubleshooting

Error Handling

If your validator function raises a ValueError or AssertionError, pydantic-core automatically catches it and converts it into a ValidationError. Other exceptions will bubble up normally.

Function Signatures

Ensure your function signature matches the expected type:

  • No Info: def fn(value: Any) -> Any (or def fn(value, handler) for wrap)
  • With Info: def fn(value: Any, info: ValidationInfo) -> Any (or def fn(value, handler, info) for wrap)

Using the wrong signature will result in a TypeError at runtime.

Deprecated field_name Argument

When creating schemas using functions like with_info_before_validator_function, the field_name keyword argument is deprecated. The field name is now automatically populated in the ValidationInfo object when the validator is used within a model or TypedDict.