Skip to content
GitHub
Test Failures

Moving validation from handler to Pydantic Field changes HTTP 400 to 422

Pydantic Field Validation Changes HTTP Status from 400 to 422

Severity: P2 (semantic change visible to API consumers) · Applies to: FastAPI route handlers migrating from manual validation to Pydantic Field constraints Related: ADR-006 — API versioning and RESTful conventions

Problem

After moving input validation from the route handler body into the Pydantic request model (using Field(pattern=...) or similar), tests that check for HTTP 400 start failing because FastAPI now returns HTTP 422.

Symptoms:

  • assert resp.status_code == 400 fails with 422 == 400
  • The validation itself works correctly (invalid input is rejected)
  • Only affects tests, not functionality

Root Cause

FastAPI uses two different HTTP status codes for validation:

Validation locationStatus codeReason
Route handler (raise HTTPException(400))400 Bad RequestDeveloper-defined error
Pydantic model (Field(pattern=...))422 Unprocessable EntityFastAPI’s RequestValidationError

When validation moves from handler → model, the error is raised during request deserialization (before the handler runs), and FastAPI’s default exception handler returns 422.

# Before: handler validation → 400
@router.post("/endpoint")
def handler(req: MyRequest):
if req.mode not in ("a", "b"):
raise HTTPException(status_code=400, detail="Invalid mode")
# After: model validation → 422
class MyRequest(BaseModel):
mode: str = Field(pattern=r"^(a|b)$")
@router.post("/endpoint")
def handler(req: MyRequest):
pass # Invalid mode never reaches here

Solution

  1. Update tests to expect 422 instead of 400:
# Before
def test_invalid_input_returns_400(self):
resp = client.post("/endpoint", json={"mode": "invalid"})
assert resp.status_code == 400
# After
def test_invalid_input_returns_422(self):
resp = client.post("/endpoint", json={"mode": "invalid"})
assert resp.status_code == 422
  1. Remove redundant handler validation — the manual check is now dead code since Pydantic rejects invalid input before the handler runs.

Prevention

Best Practices

  • Prefer Pydantic model validation over handler-level checks — it’s more declarative, generates OpenAPI schema, and catches errors earlier
  • Accept 422 as the correct status for request validation errors in FastAPI — this is the framework convention and matches the OpenAPI spec
  • When migrating validation, update tests in the same commit to avoid breaking CI
  • Remove the old handler validation to avoid dead code

Warning Signs

  • Tests that check for status_code == 400 on input validation
  • Duplicate validation (both in Pydantic model and route handler)
  • HTTPException(400) for problems that are really request schema violations

References