FastAPI: Handling Pydantic RequestValidationError and Custom Exceptions
This error occurs when Python tries to instantiate a Pydantic model with data that violates field type constraints or validation rules. Catch RequestValidationError with a custom exception handler in FastAPI to return formatted error responses.
Root Cause Analysis
This error occurs when Python tries to validate an incoming request payload against a Pydantic schema and one or more fields fail validation constraints or type coercion rules.
Root Cause 1: Client Payload Mismatches
When a client sends JSON data where field types do not match the Pydantic model definition (such as sending a non-numeric string for an int field, an invalid email string for an EmailStr field, or omitting a required field without a default value), Pydantic raises a ValidationError. In FastAPI, this error is caught by the framework and converted into a RequestValidationError with HTTP status 422 Unprocessable Entity.
Root Cause 2: Catching the Wrong Exception Class
A common architectural issue in FastAPI applications is attempting to register an exception handler for pydantic.ValidationError instead of fastapi.exceptions.RequestValidationError. Because FastAPI wraps raw request validation errors in RequestValidationError, handlers registered solely for ValidationError will not trigger on invalid request parameters or bodies.
Root Cause 3: Field Constraint Violations
Pydantic supports fine-grained field constraints such as gt, lt, min_length, max_length, and regex pattern. If incoming data fails any of these declarative constraints or fails custom @field_validator functions that raise ValueError, Pydantic generates a validation failure describing the field location and invalid input.
Root Cause 4: Leaking Internal Schema Details in Production
By default, FastAPI's 422 response returns raw Pydantic validation errors including internal model field names and error codes. In enterprise APIs, this can expose internal architectural details to consumers, requiring a centralized custom exception handler to sanitize and format error responses consistently.
Reproduction Code (MCVE)
from pydantic import BaseModel, Field
class UserRegistration(BaseModel):
user_id: int
username: str = Field(min_length=3)
email: str
# Attempting to instantiate the Pydantic model with invalid data types
invalid_payload = {
"user_id": "not-an-integer-id",
"username": "al",
"email": "invalid_email_address"
}
# Raw model instantiation triggers pydantic.ValidationError
UserRegistration(**invalid_payload)
Solution 1: Register a Global RequestValidationError Handler in FastAPI
Register a custom @app.exception_handler(RequestValidationError) to transform raw 422 validation errors into a standardized, client-friendly error format.
from fastapi import FastAPI, Request, status
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field
app = FastAPI()
class ItemCreate(BaseModel):
name: str = Field(..., min_length=2)
price: float = Field(..., gt=0)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
errors = []
for err in exc.errors():
field_path = " -> ".join(str(loc) for loc in err.get("loc", []))
errors.append({
"field": field_path,
"message": err.get("msg"),
"type": err.get("type")
})
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content={"success": False, "errors": errors}
)
# Standalone test of valid Pydantic instantiation
valid_item = ItemCreate(name="Notebook", price=12.99)
print("Validated successfully:", valid_item.model_dump())
Solution 2: Use Pydantic Field Validators with Custom Messages
Use @field_validator with mode='after' or mode='before' in Pydantic V2 to enforce custom business rules and sanitize field inputs gracefully.
from pydantic import BaseModel, field_validator
class UserAccount(BaseModel):
username: str
age: int
@field_validator("username")
@classmethod
def validate_username(cls, v: str) -> str:
cleaned = v.strip().lower()
if len(cleaned) < 3:
raise ValueError("Username must be at least 3 characters long after trimming whitespace")
return cleaned
@field_validator("age")
@classmethod
def validate_age(cls, v: int) -> int:
if v < 18 or v > 120:
raise ValueError("Age must be between 18 and 120")
return v
# Valid instantiation demonstrating clean transformation
account = UserAccount(username=" AlexDev ", age=28)
print("Sanitized account data:", account.model_dump())
A major misconception is treating RequestValidationError and HTTPException as interchangeable. HTTPException is raised manually by endpoint business logic (e.g. 404 Not Found or 403 Forbidden), whereas RequestValidationError is raised automatically by the dependency injection and routing engine before your endpoint code runs.
Another edge case in Pydantic V2 is the change from @validator to @field_validator. In Pydantic V2, @field_validator requires the @classmethod decorator and defaults to mode='after', meaning the input argument is already type-coerced. If you need to intercept raw un-coerced strings, set mode='before'.
Contrast Pydantic validation errors with database integrity errors (like SQLAlchemy IntegrityError): Pydantic validates payload syntax and types in memory, while database integrity errors occur when inserting records that violate uniqueness or foreign key constraints.