422 Unprocessable Content (tested)
422 Unprocessable Content means the server understood the request format but the content failed its rules: a required field is missing, a value has the wrong type, or a business check failed. The old name was Unprocessable Entity. We tested where frameworks send 422 and where they send 400 instead.
1. What 422 means
The request is well formed enough to read, but the data inside is not acceptable. Fixing it means changing the data, not the syntax or the headers. RFC 9110 moved 422 from WebDAV into core HTTP and renamed it Unprocessable Content. Both names refer to the same code.
You will see both reason phrases in the wild. In our run, uvicorn sent HTTP/1.1 422 Unprocessable Content. Node.js (Express) sent HTTP/1.1 422 Unprocessable Entity. Clients should check the number, never the phrase.
2. FastAPI: almost every bad body is a 422
We used a pydantic model with name: str, qty: int and extra="forbid":
class Item(BaseModel):
model_config = ConfigDict(extra="forbid")
name: str
qty: int
@app.post("/items")
def items(i: Item): return {"ok": True}
| Request to /items | Status | type / loc |
|---|---|---|
JSON missing qty | 422 | missing / ["body","qty"] |
"qty":"lots" | 422 | int_parsing / ["body","qty"] |
"qty":"5" (numeric string) | 200 | coerced to 5 (lax mode) |
Extra field "x":2 with extra="forbid" | 422 | extra_forbidden / ["body","x"] |
| Extra field, default model | 200 | field silently dropped |
Malformed JSON {bad | 422 | json_invalid / ["body",1] |
| No Content-Type header | 422 | model_attributes_type / ["body"] |
Form body (x-www-form-urlencoded) | 422 | model_attributes_type / ["body"] |
text/plain with JSON text | 422 | model_attributes_type / ["body"] |
| Empty POST, no body | 422 | missing / ["body"] |
GET /q?limit=ten (limit: int) | 422 | int_parsing / ["query","limit"] |
The body is always {"detail": [...]} with Content-Type: application/json. Each item has type, loc, msg and input. The missing-field case returned exactly:
{"detail":[{"type":"missing","loc":["body","qty"],
"msg":"Field required","input":{"name":"a"}}]}
Malformed JSON adds a ctx object:
{"detail":[{"type":"json_invalid","loc":["body",1],"msg":"JSON decode error",
"input":{},"ctx":{"error":"Expecting property name enclosed in double quotes"}}]}
Two surprises. FastAPI returned 422, not 400, for broken JSON. It also returned 422, not 415, for a missing or wrong Content-Type. If you want 400 or 415 there, add your own exception handler or a Content-Type check. Note that curl -d without -H sends application/x-www-form-urlencoded, so it hits the form row, not a JSON row.
3. Express: 400 for bad JSON, 422 is up to you
express.json() rejects broken JSON before your route runs. Our error handler printed what it got:
$ curl -i -H 'Content-Type: application/json' -d '{bad' :8987/users
HTTP/1.1 400 Bad Request
{"error":"entity.parse.failed","message":"Expected property name or '}' in JSON at position 1 (line 1 column 2)"}
Express never sends 422 by itself. For valid JSON that fails your rules, return it yourself. We used a problem+json body:
app.use(express.json());
app.post('/users', (req, res) => {
const b = req.body || {}; const errors = [];
if (typeof b.email !== 'string' || !b.email.includes('@'))
errors.push({ pointer: '#/email', detail: 'must be an email address' });
if (!Number.isInteger(b.age))
errors.push({ pointer: '#/age', detail: 'must be an integer' });
if (errors.length) return res.status(422).type('application/problem+json').json({
type: 'about:blank', title: 'Unprocessable Content', status: 422,
detail: 'Validation failed', errors });
res.status(201).json({ ok: true });
});
$ curl -i -H 'Content-Type: application/json' -d '{"email":"nope","age":"x"}' :8987/users
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json; charset=utf-8
{"type":"about:blank","title":"Unprocessable Content","status":422,"detail":"Validation failed",
"errors":[{"pointer":"#/email","detail":"must be an email address"},{"pointer":"#/age","detail":"must be an integer"}]}
A trap: with Content-Type: text/plain, express.json() skipped the body, req.body was undefined, and our route answered 422 with both fields "invalid". The real problem was the media type. Check req.is('application/json') first and send 415. See 415 Unsupported Media Type. Express added ; charset=utf-8 on its own; see Content-Type charset=utf-8.
4. 400 vs 415 vs 422
| Problem | Best code | What we measured |
|---|---|---|
| Body is not parseable (broken JSON) | 400 | Express 400; FastAPI 422 (json_invalid) |
| Wrong or missing Content-Type | 415 | FastAPI 422 (model_attributes_type); Express route saw no body |
| Parses, but field missing or wrong type | 422 | FastAPI 422; Express 422 from our code |
| Valid data that clashes with current state (duplicate, version) | 409 | not tested here; see 409 Conflict |
Rule of thumb: 400 when you cannot read it, 415 when you will not read that format, 422 when you read it and the values are wrong.
5. Clients: fetch does not throw on 422
In Node.js 24, fetch resolved normally with ok: false and status: 422. You must check it and read the body:
const r = await fetch(url, { method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'a' }) });
if (!r.ok) {
const err = await r.json();
// FastAPI: err.detail[i].loc / msg
console.log(r.status, err.detail.map(e => e.loc.join('.') + ': ' + e.msg).join('; '));
}
// printed: 422 body.qty: Field required
Python's urllib.request.urlopen is different: it raised HTTPError with code 422. Read e.read() for the JSON. Check the response Content-Type before parsing: ours were application/json (FastAPI) and application/problem+json (Express).
6. Who else uses 422 (from their docs, not measured)
Rails scaffolds return :unprocessable_entity (422) when a model fails validation. Laravel returns 422 with a JSON errors object when a JSON request fails validation. The GitHub REST API returns 422 with "message": "Validation Failed" and an errors array.
Frequently asked questions
Is 422 Unprocessable Entity the same as 422 Unprocessable Content?
Yes. RFC 9110 renamed it to Unprocessable Content. The code is the same. In our test uvicorn sent the new phrase and Node.js sent the old one.
Why does FastAPI return 422 for malformed JSON instead of 400?
FastAPI reports every request validation error, including JSON decode errors, as 422 with type json_invalid. We measured this with FastAPI 0.142.2. Add a custom RequestValidationError handler if you want 400.
Should I use 400 or 422 for validation errors?
Use 400 when the body cannot be parsed, 415 when the Content-Type is wrong, and 422 when it parses but the values break your rules. Express follows this by default for broken JSON (400).
Why do I get 422 when I post a form to FastAPI?
A pydantic model parameter expects a JSON object. A form body or a missing Content-Type gave 422 with type model_attributes_type and loc ["body"]. Send Content-Type: application/json, or use Form() fields.
Does fetch throw on a 422 response?
No. fetch resolved with ok false and status 422. Check r.ok and call r.json() to read the error details.
Written with AI assistance. Measured on 2026-10-02 with curl 8.14.1, Node.js 24.21.0 and Python 3.13.5: FastAPI 0.142.2 (pydantic 2.13.5, uvicorn 0.54.0) on 127.0.0.1:8986 and Express 5.2.1 with express.json() on 127.0.0.1:8987. Every status code and body below is copied from those runs. PlainHeaders is our extension; curl, Node.js, Python, FastAPI and Express are free.