What this usually means
FastAPI uses Pydantic to validate request bodies, query parameters, and path parameters against defined models. A 422 Unprocessable Entity indicates the incoming data failed validation—either a required field is missing, a data type is wrong, an enum value is invalid, or a constraint (like min_length) is violated. Under the hood, Pydantic raises a ValidationError which FastAPI catches and serializes. The most common causes are mismatches between the actual request shape and the model definition, especially when dealing with optional fields, nested models, or union types.
The first ten minutes — establish facts before touching code.
- 1curl -v -X POST http://localhost:8000/endpoint -H 'Content-Type: application/json' -d '{"test": true}' and inspect the 422 response body
- 2Enable FastAPI debug logs: set LOG_LEVEL=DEBUG and look for 'Request body:' and 'Validation error:' lines
- 3Print the raw request body in a middleware: capture await request.body() and log it
- 4Test with a minimal payload that matches the model exactly, then add fields one by one
- 5Use pydantic's BaseModel.schema() to dump the expected schema and compare with actual payload
The specific files, logs, configs, and dashboards that usually own this bug.
- searchFastAPI app logs (stdout/stderr) for request body and validation error details
- searchThe Pydantic model definition file (e.g., models.py or schemas.py)
- searchRequest payload in browser Developer Tools Network tab or curl verbose output
- searchAPI documentation at /docs or /redoc to verify expected schema
- searchMiddleware or dependency injection code that might modify the request body
- searchCustom validators (field_validator, model_validator) that could raise errors
Practical causes, not theory. These are the things you will actually find.
- warningField name mismatch: request uses 'userName' but model expects 'username'
- warningWrong data type: sending string where integer expected (e.g., 'age': '25' vs 25)
- warningMissing required field: field has no default and is not provided
- warningIncorrect nested model: outer model expects inner model with 'name' but sends 'title'
- warningUnion type mismatch: field is Union[str, int] but client sends boolean or null
- warningContent-Type header missing or wrong (e.g., application/x-www-form-urlencoded instead of application/json)
Concrete fix directions. Pick the one that matches your root cause.
- buildAlign field names: use alias generator or set alias= in Field() for snake_case vs camelCase
- buildAdd type coercion: use conint, constr, or custom validators to accept string representations
- buildMake fields optional with default=None or use Optional[type]
- buildFlatten nested structures if nesting is unnecessary
- buildAdd strict=False to Pydantic config to allow coercion from compatible types
- buildEnsure Content-Type: application/json on client side
A fix you cannot prove is a guess. Close the loop.
- verifiedRun the exact same request with curl and confirm 200 response
- verifiedCheck response body contains expected fields with correct types
- verifiedAdd a test in pytest using TestClient that sends the problematic payload and asserts 200
- verifiedMonitor application logs for absence of validation error messages
- verifiedLoad test with varied payloads to ensure no regression
Things that make this bug worse or harder to find.
- warningCatching ValidationError globally and returning 400—this masks the specific field errors
- warningUsing Any type as a workaround—defeats the purpose of validation
- warningIgnoring the 'loc' array in the error detail—it pinpoints the exact field
- warningOver-relying on default values that might hide the real issue
- warningModifying the request body after validation in middleware without updating the model
The CamelCase Crisis: 422 Errors from a Mobile Client
Timeline
- 09:15Production alert: 422 errors on /api/v1/users from mobile app
- 09:20Checked logs: validation error on field 'firstName' (field required)
- 09:25Inspected Pydantic model: field defined as 'first_name' (snake_case)
- 09:30Confirmed mobile client sends 'firstName' (camelCase)
- 09:35Web client works—uses snake_case because it's our React app
- 09:45Added alias generator to convert camelCase to snake_case
- 09:50Deployed fix to staging, tested with curl—200 OK
- 10:00Deployed to production, verified mobile requests succeed
The alert came in around 9:15 AM—a spike in 422 errors from our mobile app hitting the user creation endpoint. I pulled up the logs and saw a clear pattern: validation error on 'firstName' with 'field required'. But we had a field called 'first_name' in our Pydantic model. The mobile team had used camelCase because that's what the iOS networking library expected, while our web client used snake_case to match the backend. I'd forgotten to add an alias configuration when we built the API.
I quickly wrote a migration to add an alias generator to our BaseModel config. The fix was straightforward: using Pydantic's ConfigDict with populate_by_name=True and an alias generator function that converts camelCase to snake_case. I also updated the OpenAPI spec to show snake_case in the docs, but allowed both conventions via aliases. After a quick unit test, I deployed to staging.
The staging test passed with a curl request using camelCase fields. I deployed to production at 10:00 AM. The error rate dropped immediately. The lesson: always align on naming conventions across clients early, and use alias generators as a safety net. I also added a test that sends both naming conventions to prevent regression.
Root cause
Casing mismatch: Pydantic model used snake_case but mobile client sent camelCase, causing field to be treated as missing.
The fix
Added alias generator to Pydantic model config to automatically map camelCase input to snake_case fields.
The lesson
Always handle field naming conventions explicitly with aliases, especially when multiple clients consume the API.
FastAPI returns a JSON body with a 'detail' key containing a list of error objects. Each object has 'loc' (list of path segments), 'msg' (human-readable message), 'type' (error type like 'value_error.missing'). For nested models, 'loc' includes the parent field names, e.g., ['body', 'user', 'email']. This structure is critical for automated debugging—parse 'loc' to trace the exact field.
Common types: 'value_error.missing' for required fields, 'type_error.integer' for type mismatches, 'value_error.validator' for custom validators. Use the 'type' field to prioritize fixes: missing fields are often client-side, type errors can be either side.
Pydantic v2 introduced ConfigDict for model configuration. Key settings: 'str_to_lower' for case-insensitive strings, 'coerce_numbers_to_str' for numeric unions, 'populate_by_name' to allow both alias and original name. For 422 errors, 'extra' setting (allow, forbid, ignore) determines how unknown fields are handled—'forbid' raises an error (good for strict APIs), 'ignore' silently drops them.
For union types, use 'Union[FirstModel, SecondModel]' with a discriminator field or rely on Pydantic's smart union matching. If you get 422 on unions, check that the payload's structure uniquely identifies one model. Add a 'field_discriminator' in Config to disambiguate.
Add a middleware to log the raw request body before validation. Example: 'async def log_body_middleware(request: Request, call_next): body = await request.body(); logger.info(f"Request body: {body}"); response = await call_next(request); return response'. This shows exactly what FastAPI receives, including encoding issues.
Set 'uvicorn' log level to 'debug' to see request parsing details: 'uvicorn.run(app, log_level="debug")'. Look for lines like 'HTTP Request: POST /path body: b"..."'. Combine with response logging to see the validation error response.
A field defined as 'Optional[str]' with no default is still required—you must explicitly set 'default=None'. Many engineers assume Optional makes it optional, but it only allows None. The correct pattern: 'field: Optional[str] = None' or 'field: str = None' (with default).
For nested models, ensure the inner model is also configured with defaults if needed. A common mistake: having a nested model that itself has required fields, causing 422 even when the outer model's field is optional. Use 'Union[InnerModel, None]' with default None to make the entire field optional.
Isolate the issue by testing your Pydantic model outside FastAPI: 'MyModel(**payload)' will raise a ValidationError with the same structure. This helps determine if the problem is in the model or in FastAPI's request parsing. Use pytest with parametrize to test multiple payload variations.
For complex validation logic, write unit tests for custom validators separately. Test edge cases like empty strings, null values, and out-of-range numbers. Ensure your validators raise 'ValueError' (not custom exceptions) so Pydantic catches them correctly.
Frequently asked questions
Why does my request work in Swagger UI but fail from curl?
Swagger UI automatically sets Content-Type: application/json and sends the payload as JSON. Your curl command might be missing the header or sending form data. Also, Swagger UI uses JavaScript's fetch which serializes objects correctly, while curl might require proper escaping. Always copy the exact curl command from Swagger UI's 'Try it out' feature.
How do I return a custom error message for validation errors?
Override the default validation exception handler: from fastapi.exceptions import RequestValidationError; from starlette.exceptions import HTTPException; add an @app.exception_handler(RequestValidationError) that returns a custom JSON response. Access the original errors via exc.errors() and format them as needed.
What's the difference between 422 and 400 errors?
422 (Unprocessable Entity) is specifically for semantic validation errors—the request format is correct (valid JSON) but the data doesn't satisfy the schema. 400 (Bad Request) is for malformed syntax, like invalid JSON or missing required headers. FastAPI returns 422 for Pydantic validation failures and 400 for JSON parse errors.
How do I handle file uploads with validation errors?
File uploads via UploadFile are validated separately from body fields. If you get a 422 on file fields, check that the file is sent with the correct multipart form-data and that the field name matches. FastAPI's File() dependency does not support Pydantic models directly—use Form() for metadata and UploadFile for files.
Why does my union type cause a 422 even when the data matches one of the types?
Pydantic v2 uses strict union matching by default. If the data could match multiple types, it may fail. Use 'Union[Type1, Type2, ...]' with a discriminating field or set 'smart_union=True' in ConfigDict. Also, ensure that the data type is not accidentally coerced into a different type before matching.