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/json header
  • 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: bodyitems → index 0quantity
  • 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= not data= when sending requests with the requests library
  • Always include Content-Type: application/json header
  • Read the error response - it tells you exactly what’s wrong
  • Use Optional[type] = None for optional fields
  • The loc field 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.