The go-to resource for upgrading Python, Django, Flask, and your dependencies.

How to Fix 422 Unprocessable Entity Validation Errors in FastAPI Request Bodies


When working with FastAPI, you may encounter 422 Unprocessable Entity errors like this one: {\"detail\":[{\"loc\":[\"body\",\"age\"],\"msg\":\"value is not a valid integer\",\"type\":\"type_error.integer\"}]}. These arise from Pydantic validation failures during POST, PUT, or PATCH requests when the request body doesn’t conform to your model’s expectations—whether due to type mismatches (like string to int), missing required fields, or issues in nested structures. We can address these systematically by first understanding the validation process and then applying targeted fixes.

Root Cause: Pydantic Validation Pipeline

FastAPI relies on Pydantic’s BaseModel—such as in a parameter like user: UserCreate—to validate incoming request data. The process works like this: FastAPI parses the JSON body, Pydantic then validates against the model’s fields, types, and constraints, and returns a 422 if any check fails. This pipeline ensures data integrity before your endpoint logic runs, though it requires models that match client expectations.

Error TypeExample TriggerLoc in Detail
Type Mismatch"age": "30" → int[\"body\",\"age\"]
Missing FieldOmit email req[\"body\",\"email\"]
Nested FailInvalid sub-model[\"body\",\"address\",\"zip\"]
Constraintemail invalid regexvalue_error.str.regex
Enum Mismatch"status": \"pending\" not enumvalue_error.const
Too Long/Shortname > 50 charsvalue_error.any_str.max_length
List Invalid[\"a\",\"b\", 3] int listtype_error.list.item

The response structure is consistent: a detail array containing ValidationError objects, each with loc (field path), msg (error message), and type (error category). This detail helps pinpoint issues precisely.

Debugging with Middleware: Log the Raw Request Body

If you’re unsure what the client is sending, let’s add an HTTP middleware to log the raw request body. This reveals the exact JSON without validation interference.

from fastapi import FastAPI, Request
import json

app = FastAPI()

@app.middleware("http")
async def log_body(request: Request, call_next):
    if request.method in ("POST", "PUT", "PATCH"):
        body = await request.body()
        print("Raw body:", body.decode())
    response = await call_next(request)
    return response

Let’s test it with a curl request sending invalid data:\n\nbash\ncurl -X POST http://localhost:8000/users/ -H \"Content-Type: application/json\" -d '{\"age\": \"thirty\"}'\n\n\nYou’ll see in the console: Raw body: {"age": "thirty"}. This confirms the type mismatch without model interference.

Allowing Type Coercion and Union Types

Pydantic v2 attempts to coerce compatible types—like strings to int or float—when not in strict mode. Enabling strict mode enforces exact types, which can cause failures on minor client-side formatting differences.

Model:

from pydantic import BaseModel
from typing import Optional, Union

class UserCreate(BaseModel):
    name: str
    age: Union[int, str]  # Accept str, coerce
    email: Optional[str] = None

To enable coercion, set strict: False in the model’s config:\n\npython\nclass UserCreate(BaseModel):\n model_config = ConfigDict(strict=False)\n age: int\n\n\nNote the use of ConfigDict in Pydantic v2. This approach trades stricter type safety for flexibility—use it when clients can’t always send perfect types, but validate further if needed.

Handling Missing or Optional Fields

Omitting a required field triggers the error. You can make it optional with a default or Optional type.

class UserCreate(BaseModel):
    name: str
    age: int
    email: str = ""  # Default
    # or Optional[str] = None

Alias for JSON keys:

class UserCreate(BaseModel):
    name: str = Field(..., alias="full_name")

Validating Nested Models and Lists

Invalid sub-obj/list.

Before (fails):

class Address(BaseModel):
    street: str
    zip: int  # "123abc" fails

class UserCreate(BaseModel):
    name: str
    address: Address  # Nested fail loc: ["body", "address", "zip"]

Fix: Validators or coercion.

from pydantic import field_validator

class Address(BaseModel):
    street: str
    zip: Union[str, int]  # Coerce

@field_validator("zip")
def parse_zip(cls, v):
    return int(str(v).replace("abc", ""))

Implementing Custom Validators

Complex logic.

from pydantic import BaseModel, field_validator, ValidationError

class UserCreate(BaseModel):
    email: str

    @field_validator("email")
    @classmethod
    def validate_email(cls, v):
        if "@" not in v or ".com" not in v:
            raise ValueError("Invalid email")
        return v.lower()

Choosing Between JSON, Form, and Multipart Data

Wrong Content-Type.

# JSON: application/json → Pydantic
@app.post("/users/", response_model=User)
def create_user(user: UserCreate): ...

# Form: Form(...)
from fastapi import Form
@app.post("/users/form")
def create_user_form(name: str = Form(), age: int = Form()): ...

Working with Enums and Constraints

from enum import Enum
from pydantic import Field

class Status(str, Enum):
    active = "active"
    inactive = "inactive"

class User(BaseModel):
    status: Status
    name: str = Field(..., min_length=1, max_length=50)

Customizing Error Responses with Global Handlers

Hide raw detail.

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

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
    return JSONResponse(
        status_code=422,
        content={"detail": "Invalid input. Check types and required fields."}
    )

Full Working Example + Tests

main.py:

from fastapi import FastAPI, Depends
from pydantic import BaseModel, field_validator

app = FastAPI()

class UserCreate(BaseModel):
    name: str
    age: int
    email: str = Field(..., pattern=r"[^@]+@[^@]+\.[^@]+")

    @field_validator("age")
    @classmethod
    def age_check(cls, v):
        if v < 0 or v > 150:
            raise ValueError("Age invalid")
        return v

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

pytest test_main.py:

from fastapi.testclient import TestClient
from main import app

client = TestClient(app)

def test_valid():
    rv = client.post("/users/", json={"name": "Bob", "age": 30, "email": "bob@example.com"})
    assert rv.status_code == 200
    assert rv.json()["name"] == "Bob"

def test_bad_age():
    rv = client.post("/users/", json={"name": "Bob", "age": "thirty", "email": "bob@example.com"})
    assert rv.status_code == 422

Run: pytest → pass. uvicorn main:app --reload

Best Practices for Maintainable Validation\n\nTo build models that stand the test of time, consider these practices:\n\n- Set model_config = ConfigDict(extra='forbid') to reject unknown fields—this catches client-side changes early rather than ignoring them.\n- Leverage FastAPI’s automatic OpenAPI documentation, which exposes your model schemas to help API consumers send correct data.\n- In production, integrate logging tools like Sentry to capture and analyze validation errors, allowing you to refine models based on real usage without exposing internals to clients.\n\nKeep in mind that stricter validation enhances data quality but may increase client friction; balance based on your API’s consumers and error tolerance. Regular testing of edge cases ensures reliability over time.\n\nRelated articles: FastAPI Depends Circular Imports, Pydantic v2 Migration, FastAPI with orjson

Deploy validation-proof FastAPI today.

Sponsored by Durable Programming

Need help maintaining or upgrading your Python application? Durable Programming specializes in keeping Python apps secure, performant, and up-to-date.

Hire Durable Programming