Error Handling
PyValidX provides a robust error handling system through the ValidationException
class, which allows capturing, processing, and responding to validation errors in a structured way.
Basic ValidationException
Error Structure
from pyvalidx import ValidatedModel, field_validated, ValidationException
from pyvalidx.core import is_required, min_length
from pyvalidx.string import is_email
class UserModel(ValidatedModel):
name: str = field_validated(
is_required('Name is required'),
min_length(2, 'Name must be at least 2 characters')
)
email: str = field_validated(
is_required('Email is required'),
is_email('Invalid email format')
)
# Capture and examine errors
try:
user = UserModel(name='A', email='invalid-email')
except ValidationException as e:
print('Status Code:', e.status_code) # 400
print('Validations:', e.validations)
# {'name': 'Name must be at least 2 characters', 'email': 'Invalid email format'}
# Get as dictionary
error_dict = e.to_dict()
print('Error Dict:', error_dict)
# Get as JSON
error_json = e.to_json()
print('Error JSON:', error_json)
Multiple Errors per Field
class PasswordModel(ValidatedModel):
password: str = field_validated(
is_required('Password is required'),
min_length(8, 'Password must be at least 8 characters'),
is_strong_password('Password must contain uppercase, lowercase, numbers and symbols')
)
try:
# Password that fails multiple validations
pwd_model = PasswordModel(password='123')
except ValidationException as e:
print(e.validations)
# {'password': 'Password must be at least 8 characters'}
# Note: Only the first failing error is reported
Custom Error Handling
Custom Status Codes
class ValidationException(Exception):
def __init__(self, validations, status_code=400):
# Default status_code is 400, but can be customized
pass
# To create errors with specific codes
def validate_admin_user(data):
try:
return AdminUserModel(**data)
except ValidationException as e:
# Re-raise with 403 code for authorization errors
if 'admin_key' in e.validations:
raise ValidationException(e.validations, status_code=403)
raise # Re-raise with original code
Error Handling Wrapper
from typing import Dict, Any, Tuple, Optional
import json
class ValidationErrorHandler:
'''
Centralized validation error handler
'''
@staticmethod
def handle_validation_error(e: ValidationException) -> Dict[str, Any]:
'''
Converts ValidationException to standard response format
Args:
e (ValidationException): ValidationException to handle
Returns:
Dict[str, Any]: Standard response format
'''
return {
'success': False,
'status_code': e.status_code,
'message': 'Validation failed',
'errors': e.validations,
'error_count': len(e.validations)
}
@staticmethod
def safe_validate(model_class, data: Dict[str, Any]) -> Tuple[Optional[Any], Optional[Dict[str, Any]]]:
'''
Safe validation that returns tuple (model, error)
Args:
model_class (type): Model class to validate
data (Dict[str, Any]): Data to validate
Returns:
Tuple[Optional[Any], Optional[Dict[str, Any]]]: Tuple of (model, error)
'''
try:
model = model_class(**data)
return model, None
except ValidationException as e:
return None, ValidationErrorHandler.handle_validation_error(e)
Contextual Errors and Debugging
Debug Information in Errors
class DebugValidationException(ValidationException):
'''
Extended ValidationException with debug information
'''
def __init__(self, validations, status_code=400, debug_info=None):
super().__init__(validations, status_code)
self.debug_info = debug_info or {}
def to_dict(self) -> Dict[str, Any]:
'''
Converts exception to dictionary including debug info
Returns:
Dict[str, Any]: Dictionary representation of the exception including debug info
'''
result = super().to_dict()
if self.debug_info:
result['debug_info'] = self.debug_info
return result
Validation Error Logging
import logging
from datetime import datetime
class ValidationLogger:
'''
Logger for validation errors
'''
def __init__(self):
self.logger = logging.getLogger('pyvalidx.validation')
def log_validation_error(self, model_name: str, errors: dict, context: dict = None):
'''
Log validation errors with context
Args:
model_name (str): Name of the model
errors (dict): Validation errors
context (dict, optional): Additional context for logging
'''
log_entry = {
'timestamp': datetime.now().isoformat(),
'model': model_name,
'errors': errors,
'context': context or {}
}
self.logger.error(f'Validation failed: {log_entry}')
Error Handling in Web Applications
FastAPI Integration
from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse
import uvicorn
app = FastAPI()
@app.exception_handler(ValidationException)
async def validation_exception_handler(request, exc: ValidationException) -> JSONResponse:
'''
Global handler for ValidationException in FastAPI
Args:
request (Request): FastAPI request object
exc (ValidationException): ValidationException to handle
Returns:
JSONResponse: JSON response with error details
'''
return JSONResponse(
status_code=exc.status_code,
content={
'message': 'Validation failed',
'errors': exc.validations,
'status_code': exc.status_code
}
)
@app.post('/users/')
async def create_user(user_data: dict):
# ValidationException will be caught automatically
user = UserModel(**user_data)
return {'message': 'User created', 'user': user.model_dump()}
# Usage:
# POST /users/ with invalid data will automatically return 400 error
Flask Integration
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.errorhandler(ValidationException)
def handle_validation_error(e) -> tuple[dict, int]:
'''
Global handler for ValidationException in Flask
Args:
e (ValidationException): ValidationException to handle
Returns:
tuple: Tuple of (response, status_code)
'''
return jsonify(e.to_dict()), e.status_code
@app.route('/users', methods=['POST'])
def create_user():
user_data = request.get_json()
# ValidationException will be caught automatically
user = UserModel(**user_data)
return jsonify({'message': 'User created', 'user': user.model_dump()})
# Usage:
# POST /users with invalid data will automatically return 400 error
Advanced Error Handling Patterns
Error Accumulation from Multiple Models
class BatchValidationResult:
'''
Result container for batch validation
'''
def __init__(self):
self.valid_models = []
self.invalid_models = []
self.errors = {}
@property
def valid_count(self) -> int:
return len(self.valid_models)
@property
def invalid_count(self) -> int:
return len(self.invalid_models)
def batch_validate(model_class, data_list: list) -> BatchValidationResult:
'''
Validate multiple instances and collect all errors
Args:
model_class (type): Model class to validate
data_list (list): List of data to validate
Returns:
BatchValidationResult: Result container with validation results
'''
result = BatchValidationResult()
for i, data in enumerate(data_list):
try:
model = model_class(**data)
result.valid_models.append(model)
except ValidationException as e:
result.invalid_models.append(data)
result.errors[f"item_{i}"] = e.validations
return result
# Usage
users_data = [
{'name': 'John', 'email': 'john@example.com'},
{'name': 'A', 'email': 'invalid-email'},
{'name': 'Jane', 'email': 'jane@example.com'}
]
batch_result = batch_validate(UserModel, users_data)
print(f'Valid: {batch_result.valid_count}, Invalid: {batch_result.invalid_count}')
Retry with Backoff for External Validations
import time
import random
from functools import wraps
def retry_validation(max_retries=3, backoff_factor=1.0) -> Callable[[Any, Optional[Dict[str, Any]]], bool]:
'''
Decorator for retrying validations with exponential backoff
Args:
max_retries (int, optional): Maximum number of retries. Defaults to 3.
backoff_factor (float, optional): Backoff factor for exponential backoff. Defaults to 1.0.
Returns:
Callable: Decorated validator function
'''
def decorator(validator_func):
@wraps(validator_func)
def wrapper(value, context=None):
last_exception = None
for attempt in range(max_retries + 1):
try:
return validator_func(value, context)
except Exception as e:
last_exception = e
if attempt < max_retries:
delay = backoff_factor * (2 ** attempt) + random.uniform(0, 1)
time.sleep(delay)
else:
raise last_exception
return wrapper
return decorator
# Validator that queries external service with retry
@retry_validation(max_retries=3)
def validate_with_external_service(value, context=None) -> bool:
'''
Example validator that queries an external service
Args:
value (Any): Value to validate
context (Dict[str, Any], optional): Context containing other field values
Returns:
bool: True if validation passes, False otherwise
'''
if value is None:
return True
# Simulate external service call that may fail
import requests
try:
response = requests.get(f'https://api.example.com/validate/{value}', timeout=5)
return response.status_code == 200
except requests.RequestException:
raise # Will be retried automatically
class ExternalValidatedModel(ValidatedModel):
external_id: str = field_validated(
custom(validate_with_external_service, 'External validation failed')
)
The robust handling of errors is crucial for creating reliable applications that can diagnose and respond appropriately to validation problems, providing useful information for both developers and end-users.