Skip to content

Fix: FastAPI 422 Unprocessable Entity (validation error)

FixDevs ·

Quick Answer

How to fix FastAPI 422 Unprocessable Entity error caused by wrong request body format, missing fields, type mismatches, query parameter errors, and Pydantic validation.

The Error

You call a FastAPI endpoint and get:

{
  "detail": [
    {
      "type": "missing",
      "loc": ["body", "name"],
      "msg": "Field required",
      "input": {}
    }
  ]
}

With HTTP status 422 Unprocessable Entity.

Or variations:

{
  "detail": [
    {
      "type": "string_type",
      "loc": ["body", "age"],
      "msg": "Input should be a valid string",
      "input": 25
    }
  ]
}
{
  "detail": [
    {
      "type": "missing",
      "loc": ["query", "page"],
      "msg": "Field required"
    }
  ]
}

FastAPI validated the incoming request data against the expected schema and found errors. The request body, query parameters, or path parameters do not match what the endpoint expects.

Why This Happens

FastAPI uses Pydantic models to validate all incoming data automatically. When the data does not match the declared types and constraints, FastAPI returns a 422 error with details about what is wrong.

The 422 response includes:

  • loc — Where the error is: ["body", "field_name"], ["query", "param_name"], or ["path", "param_name"].
  • msg — Human-readable error description.
  • type — Error type code.
  • input — The actual value that was received.

Common causes:

  • Missing required fields. The request body is missing a field the model requires.
  • Wrong data type. Sending a string where an integer is expected.
  • Wrong Content-Type. Sending form data instead of JSON, or vice versa.
  • Sending data in the wrong place. Query param instead of body, or body instead of path param.
  • Nested model validation. A nested object has invalid fields.
  • Enum or constraint violations. A value outside the allowed range or not in the enum.

Fix 1: Send the Correct Request Body

Match your request to the Pydantic model:

Endpoint definition:

from pydantic import BaseModel

class UserCreate(BaseModel):
    name: str
    email: str
    age: int

@app.post("/users/")
async def create_user(user: UserCreate):
    return {"user": user}

Broken — missing required field:

curl -X POST http://localhost:8000/users/ \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice", "email": "alice@example.com"}'
# 422: age is required

Fixed — include all required fields:

curl -X POST http://localhost:8000/users/ \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice", "email": "alice@example.com", "age": 30}'

Broken — wrong type:

curl -X POST http://localhost:8000/users/ \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice", "email": "alice@example.com", "age": "thirty"}'
# 422: age should be a valid integer

Pro Tip: FastAPI auto-generates interactive API docs at /docs (Swagger UI) and /redoc. Open http://localhost:8000/docs to see the exact schema for every endpoint and test requests interactively.

Fix 2: Set the Correct Content-Type

FastAPI expects JSON by default for request bodies:

Broken — missing Content-Type:

curl -X POST http://localhost:8000/users/ \
  -d '{"name": "Alice"}'
# 422: FastAPI can't parse the body as JSON

Fixed — add Content-Type header:

curl -X POST http://localhost:8000/users/ \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice", "email": "alice@example.com", "age": 30}'

In JavaScript:

// Wrong — sends form data by default
fetch('/users/', {
  method: 'POST',
  body: JSON.stringify({ name: 'Alice', email: 'alice@example.com', age: 30 }),
});

// Fixed — set Content-Type
fetch('/users/', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'Alice', email: 'alice@example.com', age: 30 }),
});

For form data, use Form instead of Pydantic models:

from fastapi import Form

@app.post("/login/")
async def login(username: str = Form(), password: str = Form()):
    return {"username": username}
curl -X POST http://localhost:8000/login/ \
  -d "username=alice&password=secret"

Fix 3: Fix Optional Fields

Make fields optional with default values:

from typing import Optional
from pydantic import BaseModel

class UserCreate(BaseModel):
    name: str                          # Required
    email: str                         # Required
    age: Optional[int] = None          # Optional, defaults to None
    bio: str = ""                      # Optional, defaults to empty string
    role: str = "user"                 # Optional with default

Now these all work:

{"name": "Alice", "email": "alice@example.com"}
{"name": "Alice", "email": "alice@example.com", "age": 30}
{"name": "Alice", "email": "alice@example.com", "age": null, "bio": "Hello"}

Fix 4: Fix Query Parameter Errors

Query parameters have the same validation:

@app.get("/users/")
async def list_users(page: int = 1, limit: int = 10):
    return {"page": page, "limit": limit}

Broken — wrong type in query param:

curl "http://localhost:8000/users/?page=abc"
# 422: page should be a valid integer

Fixed:

curl "http://localhost:8000/users/?page=2&limit=20"

Optional query parameters:

from typing import Optional

@app.get("/search/")
async def search(
    q: str,                           # Required query param
    page: int = 1,                    # Optional with default
    category: Optional[str] = None,   # Optional, can be omitted
):
    return {"q": q, "page": page, "category": category}

Fix 5: Fix Path Parameter Errors

@app.get("/users/{user_id}")
async def get_user(user_id: int):
    return {"user_id": user_id}

Broken:

curl http://localhost:8000/users/abc
# 422: user_id should be a valid integer

Fixed:

curl http://localhost:8000/users/123

Use Path for validation:

from fastapi import Path

@app.get("/users/{user_id}")
async def get_user(user_id: int = Path(gt=0, description="User ID")):
    return {"user_id": user_id}

Fix 6: Fix Nested Model Validation

Nested Pydantic models validate recursively:

class Address(BaseModel):
    street: str
    city: str
    zip_code: str

class UserCreate(BaseModel):
    name: str
    address: Address

@app.post("/users/")
async def create_user(user: UserCreate):
    return user

Broken — missing nested field:

{
  "name": "Alice",
  "address": {
    "street": "123 Main St",
    "city": "Springfield"
  }
}
{
  "detail": [
    {
      "loc": ["body", "address", "zip_code"],
      "msg": "Field required"
    }
  ]
}

Fixed:

{
  "name": "Alice",
  "address": {
    "street": "123 Main St",
    "city": "Springfield",
    "zip_code": "62704"
  }
}

Fix 7: Add Custom Validation

Use Pydantic validators for complex rules:

from pydantic import BaseModel, field_validator, EmailStr

class UserCreate(BaseModel):
    name: str
    email: EmailStr
    age: int

    @field_validator('name')
    @classmethod
    def name_must_not_be_empty(cls, v):
        if not v.strip():
            raise ValueError('Name cannot be empty')
        return v.strip()

    @field_validator('age')
    @classmethod
    def age_must_be_valid(cls, v):
        if v < 0 or v > 150:
            raise ValueError('Age must be between 0 and 150')
        return v

Use Field constraints:

from pydantic import BaseModel, Field

class UserCreate(BaseModel):
    name: str = Field(min_length=1, max_length=100)
    email: str = Field(pattern=r'^[\w.-]+@[\w.-]+\.\w+$')
    age: int = Field(ge=0, le=150)

Fix 8: Customize Error Responses

Override the default 422 response format:

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError

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"],
        })
    return JSONResponse(
        status_code=422,
        content={
            "error": "Validation failed",
            "details": errors,
        },
    )

Still Not Working?

Check the /docs endpoint. FastAPI’s auto-generated Swagger UI shows the exact expected schema for every endpoint. Test your request there first.

Check for Pydantic v1 vs v2 differences. FastAPI 0.100+ uses Pydantic v2 by default. Some model definitions changed:

# Pydantic v1
class User(BaseModel):
    class Config:
        orm_mode = True

# Pydantic v2
class User(BaseModel):
    model_config = ConfigDict(from_attributes=True)

Check for file upload issues. File uploads need File and UploadFile:

from fastapi import File, UploadFile

@app.post("/upload/")
async def upload(file: UploadFile = File()):
    return {"filename": file.filename}

For Python async errors, see Fix: Python RuntimeError: no running event loop. For JSON parsing errors, see Fix: Python JSONDecodeError: Expecting value.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles