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 with extra="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 Exception instead of ValidationError — 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 .