FastAPI uses Pydantic for request validation, and when validation fails, you get a 422 Unprocessable Entity response. This is one of the most common errors FastAPI developers encounter, especially when building APIs that accept JSON data.
TLDR - Quick Fix (90% of Cases)
Getting 422 Unprocessable Entity? Here’s what to check first:
# ❌ BAD - Missing Content-Type header
import requests
requests.post("/users", data={"name": "Alice"})
# ✅ GOOD - Send JSON with correct header
requests.post("/users", json={"name": "Alice"})
# ❌ BAD - Wrong field type
{"age": "twenty"} # age should be int
# ✅ GOOD - Correct field type
{"age": 20}
# ❌ BAD - Missing required field
{"name": "Alice"} # missing 'email' field
# ✅ GOOD - All required fields present
{"name": "Alice", "email": "alice@example.com"}
Most common causes:
- Sending form data instead of JSON
- Missing
Content-Type: application/jsonheader - Wrong data type for a field (string instead of int)
- Missing required fields in request body
- Typo in field names (case-sensitive)
Understanding 422 Unprocessable Entity
The 422 Unprocessable Entity status code means the server understands the request format, but cannot process it due to semantic errors. In FastAPI, this almost always means Pydantic validation failed.
Here’s a typical error response:
{
"detail": [
{
"type": "missing",
"loc": ["body", "email"],
"msg": "Field required",
"input": {"name": "Alice"}
}
]
}
The response tells you:
- type: The validation error type (
missing,string_type,int_parsing, etc.) - loc: Where the error occurred (
body,query,path, field name) - msg: Human-readable error message
- input: The actual input that was received
Common Causes of 422 Errors
1. Sending Form Data Instead of JSON
The most common mistake - using data instead of json in requests:
from pydantic import BaseModel
from fastapi import FastAPI
app = FastAPI()
class User(BaseModel):
name: str
email: str
@app.post("/users")
def create_user(user: User):
return {"message": f"Created {user.name}"}
import requests
# ❌ BAD - Sends as form data (application/x-www-form-urlencoded)
response = requests.post(
"http://localhost:8000/users",
data={"name": "Alice", "email": "alice@example.com"}
)
# Result: 422 Unprocessable Entity
# ✅ GOOD - Sends as JSON (application/json)
response = requests.post(
"http://localhost:8000/users",
json={"name": "Alice", "email": "alice@example.com"}
)
# Result: 200 OK
2. Missing Required Fields
Pydantic requires all fields without defaults:
class CreateOrder(BaseModel):
product_id: int
quantity: int
shipping_address: str # Required!
# ❌ Missing shipping_address
{"product_id": 123, "quantity": 2}
# Error: Field required at loc: ["body", "shipping_address"]
# ✅ All fields provided
{"product_id": 123, "quantity": 2, "shipping_address": "123 Main St"}
Fix by making fields optional or providing defaults:
from typing import Optional
class CreateOrder(BaseModel):
product_id: int
quantity: int = 1 # Default value
shipping_address: Optional[str] = None # Optional field
3. Wrong Data Types
Pydantic strictly validates types:
class Product(BaseModel):
name: str
price: float
in_stock: bool
# ❌ Wrong types
{
"name": "Laptop",
"price": "expensive", # Should be number
"in_stock": "yes" # Should be boolean
}
# Error: Input should be a valid number
# ✅ Correct types
{
"name": "Laptop",
"price": 999.99,
"in_stock": true
}
4. Path and Query Parameter Errors
422 errors also occur with path and query parameters:
@app.get("/users/{user_id}")
def get_user(user_id: int):
return {"user_id": user_id}
# ❌ Non-integer path parameter
# GET /users/abc
# Error: Input should be a valid integer at loc: ["path", "user_id"]
# ✅ Valid integer
# GET /users/123
@app.get("/search")
def search(limit: int = 10, offset: int = 0):
return {"limit": limit, "offset": offset}
# ❌ Invalid query parameter
# GET /search?limit=all
# Error: Input should be a valid integer at loc: ["query", "limit"]
# ✅ Valid query parameters
# GET /search?limit=20&offset=0
5. Nested Model Validation
Errors in nested models can be confusing:
class Address(BaseModel):
street: str
city: str
zip_code: str
class Customer(BaseModel):
name: str
address: Address
# ❌ Missing nested field
{
"name": "Alice",
"address": {
"street": "123 Main St",
"city": "Boston"
# missing zip_code
}
}
# Error at loc: ["body", "address", "zip_code"]
# ✅ Complete nested object
{
"name": "Alice",
"address": {
"street": "123 Main St",
"city": "Boston",
"zip_code": "02101"
}
}
6. List Validation Errors
When validating lists of items:
class Item(BaseModel):
name: str
quantity: int
class Order(BaseModel):
items: list[Item]
# ❌ Invalid item in list
{
"items": [
{"name": "Apple", "quantity": 5},
{"name": "Banana", "quantity": "three"} # Invalid!
]
}
# Error at loc: ["body", "items", 1, "quantity"]
# ✅ All items valid
{
"items": [
{"name": "Apple", "quantity": 5},
{"name": "Banana", "quantity": 3}
]
}
Debugging 422 Errors
1. Read the Error Response Carefully
The error response contains all the information you need:
{
"detail": [
{
"type": "int_parsing",
"loc": ["body", "items", 0, "quantity"],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "five"
}
]
}
This tells you:
- Error type:
int_parsing- failed to parse as integer - Location:
body→items→ index0→quantity - The actual input was
"five"
2. Enable Request Logging
Add middleware to log incoming requests:
from fastapi import FastAPI, Request
import json
app = FastAPI()
@app.middleware("http")
async def log_requests(request: Request, call_next):
# Log request details
body = await request.body()
print(f"Method: {request.method}")
print(f"URL: {request.url}")
print(f"Headers: {dict(request.headers)}")
print(f"Body: {body.decode()}")
response = await call_next(request)
return response
3. Check Content-Type Header
Ensure the client sends the correct header:
# Using curl
curl -X POST "http://localhost:8000/users" \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "email": "alice@example.com"}'
# Using JavaScript fetch
fetch('/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: 'Alice', email: 'alice@example.com' })
});
4. Validate JSON Syntax
Invalid JSON causes different errors but can be confusing:
# ❌ Invalid JSON (single quotes)
{'name': 'Alice'}
# ❌ Invalid JSON (trailing comma)
{"name": "Alice",}
# ✅ Valid JSON
{"name": "Alice"}
Custom Error Responses
Override Default Validation Error Handler
Customize the 422 response format:
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
app = FastAPI()
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
errors = []
for error in exc.errors():
field = " -> ".join(str(loc) for loc in error["loc"])
errors.append({
"field": field,
"message": error["msg"],
"type": error["type"]
})
return JSONResponse(
status_code=422,
content={
"success": False,
"message": "Validation failed",
"errors": errors
}
)
Add Helpful Error Messages
Use Pydantic’s Field for better error messages:
from pydantic import BaseModel, Field
class User(BaseModel):
name: str = Field(
...,
min_length=2,
max_length=50,
description="User's full name"
)
age: int = Field(
...,
ge=0,
le=150,
description="User's age in years"
)
email: str = Field(
...,
pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$',
description="Valid email address"
)
Common Patterns and Solutions
Accepting Both Form and JSON Data
from fastapi import FastAPI, Form, Body
from typing import Annotated
app = FastAPI()
# Separate endpoints for different content types
@app.post("/users/json")
def create_user_json(user: User):
return user
@app.post("/users/form")
def create_user_form(
name: Annotated[str, Form()],
email: Annotated[str, Form()]
):
return {"name": name, "email": email}
Making All Fields Optional for Updates
from typing import Optional
class UserCreate(BaseModel):
name: str
email: str
age: int
class UserUpdate(BaseModel):
name: Optional[str] = None
email: Optional[str] = None
age: Optional[int] = None
@app.post("/users")
def create_user(user: UserCreate):
return user
@app.patch("/users/{user_id}")
def update_user(user_id: int, user: UserUpdate):
# Only update provided fields
update_data = user.model_dump(exclude_unset=True)
return {"user_id": user_id, "updated": update_data}
Handling Optional Request Body
from typing import Optional
@app.post("/process")
def process_data(data: Optional[User] = None):
if data is None:
return {"message": "No data provided"}
return {"message": f"Processing {data.name}"}
Testing API Endpoints
Using TestClient
from fastapi.testclient import TestClient
client = TestClient(app)
def test_create_user_success():
response = client.post(
"/users",
json={"name": "Alice", "email": "alice@example.com"}
)
assert response.status_code == 200
def test_create_user_validation_error():
response = client.post(
"/users",
json={"name": "Alice"} # Missing email
)
assert response.status_code == 422
assert "email" in str(response.json())
Using curl for Debugging
# Correct request
curl -X POST "http://localhost:8000/users" \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "email": "alice@example.com"}'
# See detailed error response
curl -X POST "http://localhost:8000/users" \
-H "Content-Type: application/json" \
-d '{"name": "Alice"}' | python -m json.tool
Frequently Asked Questions
What does 422 Unprocessable Entity mean in FastAPI?
It means your request data failed Pydantic validation. The server understood your request format but the data doesn’t match the expected schema - wrong types, missing required fields, or failed custom validators.
Why am I getting 422 when my JSON looks correct?
Most likely you’re sending form data instead of JSON. Use json= parameter in requests library, not data=. Also check that your Content-Type header is application/json.
How do I make a field optional in FastAPI/Pydantic?
Use Optional[type] with a default value: email: Optional[str] = None. Or use type | None = None in Python 3.10+.
How do I see what data FastAPI actually received?
Add logging middleware to print request body, or use FastAPI’s dependency injection to access the raw request body before validation.
Can I customize the 422 error response format?
Yes, use @app.exception_handler(RequestValidationError) to create a custom error response format that matches your API style.
Summary
The 422 Unprocessable Entity error in FastAPI indicates Pydantic validation failure. It’s not a bug - it’s FastAPI protecting your application from invalid data.
Key takeaways:
- Use
json=notdata=when sending requests with the requests library - Always include
Content-Type: application/jsonheader - Read the error response - it tells you exactly what’s wrong
- Use
Optional[type] = Nonefor optional fields - The
locfield in errors shows the exact path to the problem - Test your endpoints with both valid and invalid data
Understanding 422 errors is essential for building robust FastAPI applications. The detailed error messages Pydantic provides make debugging straightforward once you know where to look.
For understanding how validation errors appear in stack traces, check out our Python Traceback guide to learn how to read error messages effectively.
Having trouble with complex Python errors? Try Debugly’s stack trace formatter to automatically format and highlight the most important information in your tracebacks.