422 Unprocessable Content
Client error response defined by RFC 9110 §15.5.21.
Written and maintained by Ben Ennis
Last reviewed July 27, 2026 · How we verify this
Common causes at a glance
- JSON parsed fine but a field failed a validation rule
- A required property missing, or an enum given an unknown value
- A framework default: Rails, Laravel and FastAPI all return 422 for validation errors
- Business-rule rejection such as a duplicate email or an out-of-range date
Reproduce it
curl -i -X POST -H 'Content-Type: application/json' -d '{"email":"nope"}' https://api.example.com/v1/usersWhat 422 Unprocessable Content means
The distinction 422 draws is between parsing and understanding. A body that
is not valid JSON at all earns 400; a body the server cannot parse
because it does not speak the media type earns 415; a body that
parses cleanly but says "age": -3 earns 422. The payload simply does not
describe a state the server is willing to accept.
RFC 9110 renamed the code. WebDAV registered it as “Unprocessable Entity” and that phrase is still what most libraries print, but the currently registered phrase is “Unprocessable Content”. Servers may send either; clients must key off the number.
What the spec says
422 was originally RFC 4918 §11.2 and applied to XML
request bodies in WebDAV. RFC 9110 §15.5.21 generalised it to any
content type and renamed it. The definition is deliberately thin: the content type was
understood, the syntax is correct, and the instructions could not be processed. Nothing in
the spec dictates the error body. That gap is what RFC 9457 fills — its
application/problem+json object, with the extension members in
§3.2, is the closest thing to a standard shape for validation
output.
What actually causes it
Most 422s in the wild come from a framework’s default rather than a
deliberate design decision. Rails returns 422 from
render json: obj.errors, status: :unprocessable_entity, Laravel’s
ValidationException does the same, and FastAPI returns 422 for any Pydantic
model that fails to validate — including a malformed path parameter, which is arguably a
400. That fuels a long-running argument. One camp reads RFC 9110 narrowly and says 400
covers everything a client got wrong; the other wants the extra signal so a client can
distinguish “retry with different data” from “your request was garbage”. Pick one and
stay consistent, because clients will branch on it.
How to debug it
Read the body before anything else. A well-behaved 422 names the field that failed, and the response is where all the information lives — there is no header convention for validation errors.
curl -sS -i -X POST -H 'Content-Type: application/json' \
-d '{"email":"nope","age":-3}' https://api.example.com/v1/users
If the body is empty or a generic string, the next question is whether the request even
reached the handler. Framework-level validation rejects before application code runs, so
your own logs may show nothing. Confirm the Content-Type is what the server
expects: sending application/x-www-form-urlencoded to a JSON endpoint often
yields 422 rather than 415, because the parser produced an empty object that then failed
validation. Then diff a known-good payload against the failing one field by field.
Why a 422 must never be served from cache
RFC 9111 §4.2.2 lists the response status codes a cache may store and reuse without explicit freshness information: 200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, and 501. 422 is not on that list, and neither is 400 or 409. That omission is not an oversight — a validation failure is a judgement about the specific request body you sent, not a durable fact about the URL, so there is nothing for a shared cache to usefully reuse. Two different POST bodies to the same endpoint can produce completely different 422 outcomes, unlike a 404 or 410 that describes the resource itself regardless of what request produced it.
The practical consequence is that a 422 can only end up cached if a server explicitly
opts in with Cache-Control: max-age or similar, and doing so is almost
always a bug. A CDN or reverse-proxy cache configured to cache “any successful-looking
response regardless of status” will not touch a 422 by default, but a misconfigured rule
that caches by path while ignoring method and body will happily replay somebody else’s
validation error to an entirely different request. If you see a 422 response carrying an
Age header, or a client reporting the identical validation error across
requests it knows differ, look at cache configuration before assuming the validation
logic itself is broken — a non-cacheable status code showing cache metadata is a
configuration bug, not a validation bug.
Working examples
curl
# The body is the whole point of a 422. Print it, and the headers with it.
curl -sS -i -X POST \
-H 'Content-Type: application/json' \
-d '{"email":"nope","age":-3}' \
https://api.example.com/v1/users
# Compare against a payload you know is valid to isolate the offending field.
Python (requests)
import requests
resp = requests.post(
"https://api.example.com/v1/users",
json={"email": "nope", "age": -3},
timeout=10,
)
if resp.status_code == 422:
# RFC 9457 problem+json if you are lucky, framework-specific JSON if not.
print(resp.headers.get("Content-Type"))
print(resp.json()) # e.g. {"errors": {"email": ["is invalid"]}}
resp.raise_for_status()
Node (fetch)
const res = await fetch("https://api.example.com/v1/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "nope", age: -3 }),
});
if (res.status === 422) {
const problem = await res.json();
// Do not retry the same body; nothing about it will succeed on a second attempt.
console.error("validation failed:", problem);
}Reference and tooling
The authoritative list of assigned status codes is theIANA HTTP Status Code Registry. For a searchable copy with the semantics summarised, use theHTTP status code reference on this site. To see exactly what a server is sending, theheader inspector shows the raw status line and response headers, and thecurl builder assembles the request flags for you.
Related reading
- Every HTTP status code, and when it shows up
The wider map: which codes you actually meet in production, and the pairs everyone confuses.
Other 4xx codes
- 400 Bad Request
- 401 Unauthorized
- 402 Payment Required
- 403 Forbidden
- 404 Not Found
- 405 Method Not Allowed
- 406 Not Acceptable
- 407 Proxy Authentication Required
- 408 Request Timeout
- 409 Conflict
- 410 Gone
- 411 Length Required
- 412 Precondition Failed
- 413 Content Too Large
- 414 URI Too Long
- 415 Unsupported Media Type
- 416 Range Not Satisfiable
- 417 Expectation Failed
- 418 (Unused)
- 419 Page Expired
- 421 Misdirected Request
- 423 Locked
- 424 Failed Dependency
- 425 Too Early
- 426 Upgrade Required
- 428 Precondition Required
- 429 Too Many Requests
- 431 Request Header Fields Too Large
- 444 No Response
- 451 Unavailable For Legal Reasons
- 494 Request Header Too Large
- 495 SSL Certificate Error
- 496 SSL Certificate Required
- 497 HTTP Request Sent to HTTPS Port
- 499 Client Closed Request