Comprehensive validation framework with structured error reporting, field-level validators, custom validation functions, and detailed error location tracking.
Structured exception class that provides detailed information about validation failures with precise location tracking.
class ValidationError(Exception):
"""
Exception raised when validation fails.
Provides structured error information with location tracking
for precise error reporting and debugging.
"""
def __init__(
self,
messages: Optional[Union[ErrorMsg, Sequence[ErrorMsg]]] = None,
children: Optional[Mapping[ErrorKey, "ValidationError"]] = None,
):
"""
Initialize validation error.
Parameters:
- messages: Error messages for this location
- children: Nested validation errors for child fields/elements
"""
@property
def errors(self) -> List[LocalizedError]:
"""
Get all errors as a flat list with location information.
Returns:
List of errors with 'loc' (location path) and 'err' (error message)
"""
@staticmethod
def from_errors(errors: Sequence[LocalizedError]) -> "ValidationError":
"""
Create ValidationError from a list of localized errors.
Parameters:
- errors: List of error dictionaries with 'loc' and 'err' keys
Returns:
ValidationError instance
"""Decorator for registering validation functions that can be applied to types, fields, or during operations.
def validator(
func: Optional[Callable] = None,
*,
field: Any = None,
discard: Any = None,
owner: Optional[Type] = None,
) -> Callable:
"""
Register a validation function.
Parameters:
- func: Validation function to register
- field: Specific field to validate (default: whole object)
- discard: Fields to discard when validation passes
- owner: Owner class for field validation
Returns:
Decorated validation function
"""Configuration class for detailed validator setup with field targeting and error handling.
class Validator:
"""
Validator configuration with field targeting and options.
"""
def __init__(
self,
func: Callable,
field: Optional[FieldOrName] = None,
discard: Optional[Collection[FieldOrName]] = None,
):
"""
Initialize validator.
Parameters:
- func: Validation function
- field: Target field for validation
- discard: Fields to discard on successful validation
"""Functions for applying validators to objects and handling validation results.
def validate(
obj: T,
validators: Optional[Iterable[Validator]] = None,
kwargs: Optional[Mapping[str, Any]] = None,
*,
aliaser: Aliaser = lambda s: s,
) -> T:
"""
Apply validators to an object.
Parameters:
- obj: Object to validate
- validators: List of validators to apply
- kwargs: Additional arguments for validation functions
- aliaser: Function to transform field names
Returns:
Validated object (may be modified if fields discarded)
Raises:
ValidationError: If validation fails
"""Exception for conditionally removing fields during validation.
class Discard(Exception):
"""
Exception for discarding fields during validation.
Allows validators to conditionally remove fields from objects
while providing error information.
"""
def __init__(self, fields: Optional[AbstractSet[str]], error: ValidationError):
"""
Initialize discard exception.
Parameters:
- fields: Field names to discard
- error: Associated validation error
"""from dataclasses import dataclass
from apischema import validator, deserialize, ValidationError
@dataclass
class User:
name: str
age: int
email: str
@validator
def validate_age(age: int) -> int:
"""Validate age is reasonable."""
if age < 0:
raise ValueError("Age cannot be negative")
if age > 150:
raise ValueError("Age cannot exceed 150")
return age
@validator
def validate_email(email: str) -> str:
"""Validate email format."""
if "@" not in email:
raise ValueError("Invalid email format")
return email
# Register validators for specific fields
validator(validate_age, field="age", owner=User)
validator(validate_email, field="email", owner=User)
# Validation during deserialization
try:
user = deserialize(User, {"name": "John", "age": -5, "email": "invalid"})
except ValidationError as err:
for error in err.errors:
print(f"{error['loc']}: {error['err']}")
# ['age']: Age cannot be negative
# ['email']: Invalid email formatfrom typing import List
@dataclass
class Product:
name: str
price: float
tags: List[str]
@validator
def validate_product(product: Product) -> Product:
"""Validate entire product object."""
if product.price <= 0:
raise ValueError("Price must be positive")
if len(product.name.strip()) == 0:
raise ValueError("Name cannot be empty")
if len(product.tags) == 0:
raise ValueError("Product must have at least one tag")
return product
# Apply validator to the Product class
validator(validate_product, owner=Product)
try:
product = deserialize(Product, {
"name": "",
"price": -10,
"tags": []
})
except ValidationError as err:
print(err.errors)
# Multiple validation errors for the product object@dataclass
class Account:
type: str # "personal" or "business"
name: str
tax_id: Optional[str] = None
@validator
def validate_business_tax_id(account: Account) -> Account:
"""Business accounts must have tax ID."""
if account.type == "business" and not account.tax_id:
raise ValueError("Business accounts require tax ID")
return account
validator(validate_business_tax_id, owner=Account)
# Valid personal account
personal = deserialize(Account, {"type": "personal", "name": "John Doe"})
# Invalid business account
try:
business = deserialize(Account, {"type": "business", "name": "ACME Corp"})
except ValidationError as err:
print(err.errors) # Missing tax ID errorfrom apischema.validation import Discard
@dataclass
class UserInput:
username: str
password: str
password_confirm: str
@validator
def validate_passwords(user: UserInput) -> UserInput:
"""Validate passwords match and discard confirmation field."""
if user.password != user.password_confirm:
raise ValidationError([{
"loc": ["password_confirm"],
"err": "Passwords do not match"
}])
# Discard password_confirm field after validation
raise Discard({"password_confirm"}, ValidationError([]))
validator(validate_passwords, owner=UserInput)
# After successful validation, password_confirm is removed
user_data = {"username": "john", "password": "secret", "password_confirm": "secret"}
user = deserialize(UserInput, user_data)
# user object won't have password_confirm field@validator
def validate_username(username: str) -> str:
"""Validate username with detailed error messages."""
if len(username) < 3:
raise ValueError("Username must be at least 3 characters long")
if not username.isalnum():
raise ValueError("Username must contain only letters and numbers")
if username.lower() in ["admin", "root", "user"]:
raise ValueError("Username is reserved and cannot be used")
return username
validator(validate_username, field="username", owner=User)@dataclass
class Order:
items: List[Dict[str, Any]]
total: float
discount: float = 0.0
@validator
def validate_items_not_empty(items: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Order must have at least one item."""
if not items:
raise ValueError("Order must contain at least one item")
return items
@validator
def validate_total_positive(total: float) -> float:
"""Total must be positive."""
if total <= 0:
raise ValueError("Order total must be positive")
return total
@validator
def validate_discount_range(discount: float) -> float:
"""Discount must be between 0 and 1."""
if not 0 <= discount <= 1:
raise ValueError("Discount must be between 0 and 1")
return discount
# Register all validators
validator(validate_items_not_empty, field="items", owner=Order)
validator(validate_total_positive, field="total", owner=Order)
validator(validate_discount_range, field="discount", owner=Order)Functions for working with validators programmatically and retrieving validation information.
def get_validators(tp: AnyType) -> Sequence[Validator]:
"""
Get all validators registered for a given type.
Parameters:
- tp: The type to get validators for
Returns:
Sequence of Validator objects registered for the type
"""ErrorMsg = str # Error message string
ErrorKey = Union[str, int] # Key for error location (field name or array index)
LocalizedError = Dict[str, Any] # Error dict with 'loc' and 'err' keys
Error = Union[ErrorMsg, Tuple[Any, ErrorMsg]] # Basic error type
ValidatorResult = Generator[Error, None, T] # Generator-based validator result type
FieldOrName = Union[str, Any] # Field reference or field name
Aliaser = Callable[[str], str] # Function for transforming field names