Cheatsheet for handling Pydantic validation errors.
Catching
from pydantic import ValidationError
try:
user = User.model_validate(data)
except ValidationError as e:
print(e.errors())
print(e.json())
Error shape
e.errors()
# [
# {
# "type": "int_parsing",
# "loc": ("age",),
# "msg": "Input should be a valid integer",
# "input": "abc",
# "url": "https://errors.pydantic.dev/...",
# },
# ...
# ]
type: error category code.loc: path to the field (("body", "user", "email")).msg: human message.input: the actual value that failed.
error_count
e.error_count()
title
e.title # "ValidationError"
include / exclude details
e.errors(include_url=False, include_context=False)
e.json(indent=2)
FastAPI custom error envelope
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
@app.exception_handler(RequestValidationError)
async def custom_handler(req, exc):
return JSONResponse(
status_code=400,
content={
"error": "validation_error",
"details": [
{
"field": ".".join(str(x) for x in e["loc"][1:]),
"type": e["type"],
"message": e["msg"],
}
for e in exc.errors()
],
},
)
ValidationError from custom code
from pydantic_core import PydanticCustomError, InitErrorDetails, ValidationError
err = ValidationError.from_exception_data(
"MyValidator",
[InitErrorDetails(
type=PydanticCustomError("custom", "Custom error message: {value}"),
loc=("field",),
input="bad_value",
)],
)
raise err
Rare; usually ValueError in a validator is enough.
Error types reference
Common types:
missing— required field absent.int_parsing,float_parsing— couldn’t coerce.int_type,string_type— strict-mode wrong type.string_too_short,string_too_long.greater_than,less_than,greater_than_equal,less_than_equal.pattern_mismatch.value_error— your validator raised ValueError.union_tag_invalid— discriminated union didn’t match.extra_forbidden— unknown key withextra="forbid".
Full list: pydantic.errors.
Custom validator error message
@field_validator("age")
@classmethod
def positive(cls, v: int) -> int:
if v < 0:
raise ValueError("age must be non-negative")
return v
# Error type: value_error
# Error msg: "Value error, age must be non-negative"
Localization (per-error message)
Pydantic doesn’t natively localize. Format errors per locale in your handler:
TRANSLATIONS = {
"en": {"missing": "Field required"},
"es": {"missing": "Campo requerido"},
}
def localize(err, lang):
return TRANSLATIONS.get(lang, TRANSLATIONS["en"]).get(err["type"], err["msg"])
Logging errors
log.warning("validation_failed", errors=exc.errors(), input_sample=str(data)[:200])
Watch out: input may contain PII. Redact or hash before logging.
Re-raise as different exception
try:
User.model_validate(data)
except ValidationError as e:
raise AppValidationError(details=e.errors())
Map to your app’s exception type if needed.
400 vs 422
FastAPI default: 422 for request body validation. Many APIs prefer 400. Customize via the handler above.
Common mistakes
- Catching
Exceptioninstead ofValidationError— too broad. - Leaking validation error messages with internal details — security smell.
- Logging full input on validation error — PII risk.
- Different error envelopes per endpoint — inconsistent client UX.
Read this next
If you want my unified error envelope + i18n handler, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .