Fix FastAPI 422 Unprocessable Entity — 5 Causes With Code
FastAPI 422 means your request body failed Pydantic validation. Here are the 5 most common causes — wrong types, missing fields, nested models — and the exact fix for each.
What Does FastAPI 422 Mean?#
FastAPI 422 Unprocessable Entity means your request body, query parameters, or path parameters failed Pydantic validation. FastAPI rejected the request before your route function even ran.
Error: {"detail": [{"loc": ["body", "email"], "msg": "field required", "type": "value_error.missing"}]}
The response body is always a JSON object with a detail array. Each item in detail tells you exactly what failed: the location (loc), the
message (msg), and the type (type). Read the detail array first — it's more useful than the 422 status code.
How to Read the Error Detail#
{
"detail": [
{
"loc": ["body", "user", "age"],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
]
}
- loc = where the problem is. ["body", "user", "age"] means: in the request body → inside the user object → in the age field.
- msg = human-readable what's wrong
- type = Pydantic error type
▎ Note: When loc[0] is "query", the problem is a query parameter, not the body. When it's "path", it's a path parameter. The fix approach differs
▎ for each.
Cause 1: Missing Required Field
Pydantic model fields without a default value are required. Sending a request without them = 422.
# Model expects all three fields — no defaults
class CreateUser(BaseModel):
name: str
email: str
role: str
# ❌ Request body missing 'role'
# POST /users
# {"name": "Alice", "email": "alice@example.com"}
# → 422: field required at ["body", "role"]
▎ Fix: Either add the field to your request, or give it a default in the model.
from typing import Optional
class CreateUser(BaseModel):
name: str
email: str
role: str = "member" # Default — field now optional
Cause 2: Wrong Type Sent
Pydantic validates types strictly. Sending "25" (string) for an int field fails, even though Python itself would convert it.
class UpdateProfile(BaseModel):
user_id: int
age: int
# ❌ age sent as string
# {"user_id": 1, "age": "twenty-five"}
# → 422: value is not a valid integer at ["body", "age"]
▎ Fix: Fix the client to send the right type, OR use Pydantic's coercion by using validator or switching to int | str with a validator.
from pydantic import validator
class UpdateProfile(BaseModel):
user_id: int
age: int
@validator('age', pre=True)
def coerce_age(cls, v):
return int(v) # Accepts "25" and converts to 25
▎ Warning: Don't add coercion validators blindly. They hide client bugs. Fix the client first. Add coercion only when you genuinely can't control
▎ input format (e.g., third-party webhooks).
Cause 3: Nested Model Validation Failure
If your model contains nested models, 422 errors deep in the hierarchy have long loc paths that are easy to misread.
class Address(BaseModel):
street: str
city: str
postcode: str
class CreateOrder(BaseModel):
item_id: int
quantity: int
shipping_address: Address
# ❌ postcode missing from nested Address
# {"item_id": 5, "quantity": 2, "shipping_address": {"street": "1 Main St", "city": "London"}}
# → 422: field required at ["body", "shipping_address", "postcode"]
▎ Fix: Follow the loc path exactly. ["body", "shipping_address", "postcode"] → go to the shipping_address object in your request body → add
▎ postcode.
{
"item_id": 5,
"quantity": 2,
"shipping_address": {
"street": "1 Main St",
"city": "London",
"postcode": "EC1A 1BB"
}
}
Cause 4: Query Parameter Type Mismatch
Query parameters are always strings in HTTP. FastAPI converts them automatically, but the conversion can fail.
@app.get("/items")
async def get_items(page: int, limit: int = 20):
...
# ❌ Request: GET /items?page=first
# → 422: value is not a valid integer at ["query", "page"]
▎ Fix: Validate query params on the client before sending. For optional params, use Optional[int] = None to allow missing values.
from typing import Optional
@app.get("/items")
async def get_items(page: Optional[int] = 1, limit: Optional[int] = 20):
...
Cause 5: Sending JSON as Form Data (or Vice Versa)
FastAPI distinguishes between JSON body (Content-Type: application/json) and form data (Content-Type: application/x-www-form-urlencoded). Sending
JSON when the route expects form data — or the reverse — always causes 422.
# Route expects Form data
from fastapi import Form
@app.post("/login")
async def login(username: str = Form(...), password: str = Form(...)):
...
# ❌ Client sends JSON instead of form data
# Content-Type: application/json
# {"username": "alice", "password": "secret"}
# → 422
▎ Fix: Match Content-Type to what the route expects. For Form routes, send Content-Type: application/x-www-form-urlencoded. For JSON routes, use
▎ Content-Type: application/json and a Pydantic model as the body param.
# Route expects JSON body — use BaseModel
class LoginRequest(BaseModel):
username: str
password: str
@app.post("/login")
async def login(body: LoginRequest):
...
Debug 422 Errors in 30 Seconds
FastAPI's auto-generated docs at /docs (Swagger UI) let you test requests interactively. Use "Try it out" to see exactly what FastAPI expects —
the schema is generated directly from your Pydantic models, so it's always accurate.
For 422s in production, log the full request body alongside the validation error:
from fastapi import Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
body = await request.body()
print(f"Validation error. Body: {body}. Errors: {exc.errors()}")
return JSONResponse(status_code=422, content={"detail": exc.errors()})
This logs the raw request body and the Pydantic validation errors together — makes root cause obvious without needing to reproduce the request
manually.
Summary
┌────────────────────────┬───────────────────────────────────────────┬──────────────────────────────────────────────┐
│ Cause │ 422 detail message │ Fix │
├────────────────────────┼───────────────────────────────────────────┼──────────────────────────────────────────────┤
│ Missing required field │ field required │ Add field to request or set default in model │
├────────────────────────┼───────────────────────────────────────────┼──────────────────────────────────────────────┤
│ Wrong type │ value is not a valid integer/string/etc │ Fix client type OR add validator │
├────────────────────────┼───────────────────────────────────────────┼──────────────────────────────────────────────┤
│ Nested model failure │ long loc path │ Follow loc path into nested object │
├────────────────────────┼───────────────────────────────────────────┼──────────────────────────────────────────────┤
│ Query param type │ value is not a valid integer at ["query"] │ Fix query string or use Optional │
├────────────────────────┼───────────────────────────────────────────┼──────────────────────────────────────────────┤
│ Wrong content type │ field required for all body fields │ Match Content-Type to route definition │
└────────────────────────┴───────────────────────────────────────────┴──────────────────────────────────────────────┘
When 422s appear in production from clients you don't control, use the exception handler above to log raw bodies. Paste the error + your Pydantic
model into DebugAI — it reads both and identifies the exact field mismatch without you having to compare schemas manually.
Debug faster starting today.
Free VS Code extension. 10 sessions/day. No credit card.