FastAPI / Editorial solution guide
422 validation: find the wrong input location or shape
Diagnose a syntactically valid request that does not satisfy the declared FastAPI contract without weakening validation or exposing submitted data.
Read-only knowledge base ยท No public comments
Recognize the symptoms
A client sends valid JSON but receives 422, the endpoint is never reached, or the error points to query or header even though the body contains a similarly named value. A request can still have the wrong source, wrapper, field type, requiredness, alias, or model constraint.
Understand the likely causes
- Typical causes include wrapping a single model under an unexpected key, omitting a required query or header parameter, confusing nullability with omission, using a comma string instead of repeated keys, or expecting conversion under strict validation. Dependency parameters also contribute to the request contract, so the endpoint signature alone may not show every required input.
Read safe error structure before changing code
Record the pinned package versions, method, route, declared parameter sources, and safe error location and type. Do not log the whole submitted body, raw credentials, or unreviewed error context. Distinguish body, query, path, and header locations and follow the dependency graph that declared the field. The first complete example sends a wrong wrapper and inspects only its location, then sends the correct flat body. It uses a new isolated app and TestClient with no external server or configured storage.
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel
class Item(BaseModel):
quantity: int
app = FastAPI()
@app.post("/items")
def create(item: Item): return item.model_dump()
with TestClient(app) as client:
wrong = client.post("/items", json={"item": {"quantity": 2}})
print(wrong.status_code, wrong.json()["detail"][0]["loc"])
print(client.post("/items", json={"quantity": 2}).json())
422 ['body', 'quantity']
{'quantity': 2}
The unexpected item wrapper does not provide body.quantity. The flat body satisfies the declared single-model contract.
Compare the actual body shape
A single Pydantic model body normally expects its declared fields directly unless an explicit embedding contract changes that shape. Multiple body parameters and embed settings can create a different wrapper. Compare generated OpenAPI with the actual JSON and Content-Type instead of inferring the protocol from the Python variable name. Also inspect extra-field policy: an unexpected wrapper may be ignored or rejected, while the genuinely required inner field remains missing.
Trace headers and dependency inputs separately
A field in JSON cannot satisfy a required header simply because the names resemble one another. The second example declares an aliased header and shows that a body copy still leaves the header missing. Header names, alias behavior, underscore conversion, and validation-error locations should be checked against the pinned baseline. For query lists, repeated keys and one comma-separated value are different formats unless the application explicitly normalizes them.
from typing import Annotated
from fastapi import FastAPI, Header
from fastapi.testclient import TestClient
app = FastAPI()
@app.post("/check")
def check(value: Annotated[str, Header(alias="X-Lab-ID", min_length=3)]):
return {"received": value}
with TestClient(app) as client:
wrong = client.post("/check", json={"X-Lab-ID": "abc"})
print(wrong.status_code, wrong.json()["detail"][0]["loc"])
print(client.post("/check", headers={"X-Lab-ID": "abc"}).json())
422 ['header', 'X-Lab-ID']
{'received': 'abc'}
Only the actual HTTP header satisfies the declared source. The synthetic header is public metadata and is not treated as authentication.
Repair the agreed contract, not the symptom
If the client is wrong, send the documented source and shape. If the API declaration is wrong, change it deliberately with compatibility, schema, and client review. Check strictness, defaults, optional fields, aliases, and cross-field validators independently. Do not turn a required security input into an optional value or catch RequestValidationError and return 200 just to satisfy a frontend branch. Keep transport validation separate from domain outcomes after valid parsing, such as an absent resource or forbidden action.
Verify negative and positive requests together
Retain a minimal synthetic reproduction and assert accepted input, the original wrong shape, missing fields, wrong types, and relevant boundary combinations. Check the status and useful safe location rather than matching every version-dependent message. Verify invalid requests never reach a fake persistence or external side-effect boundary. Run the same cases after a package or model change. Neither example uses real user data, credentials, networking, or a database. The result is a predictable public contract and a client correction supported by evidence, not a broader unreviewed acceptance policy.
Verify the fix
The corrected request should produce its documented result, while the original invalid shape still fails with the expected safe classification. Confirm generated schema matches the chosen contract and dependency inputs. Test absence and wrong-type cases without exposing their values, and verify no unintended side effect occurred before rejection.
Keep it from recurring
- Use explicit parameter sources and narrow models, keep schema and client examples versioned, and test public aliases and wrapper behavior. Treat dependency upgrades as contract changes requiring relevant regression checks. Preserve redaction when adding diagnostic handlers instead of returning the raw submitted request.