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 == 400fails with422 == 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 location | Status code | Reason |
|---|---|---|
Route handler (raise HTTPException(400)) | 400 Bad Request | Developer-defined error |
Pydantic model (Field(pattern=...)) | 422 Unprocessable Entity | FastAPI’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 → 422class MyRequest(BaseModel): mode: str = Field(pattern=r"^(a|b)$")
@router.post("/endpoint")def handler(req: MyRequest): pass # Invalid mode never reaches hereSolution
- Update tests to expect 422 instead of 400:
# Beforedef test_invalid_input_returns_400(self): resp = client.post("/endpoint", json={"mode": "invalid"}) assert resp.status_code == 400
# Afterdef test_invalid_input_returns_422(self): resp = client.post("/endpoint", json={"mode": "invalid"}) assert resp.status_code == 422- 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 == 400on input validation - Duplicate validation (both in Pydantic model and route handler)
HTTPException(400)for problems that are really request schema violations
References
- FastAPI: Handling Errors
- RFC 4918: 422 Unprocessable Entity
- Fixed in session 2026-03-21 (review findings)