TL;DR: Return 400 when the server could not parse the request (broken JSON, bad framing, unreadable parameters). Return 422 when the request parsed fine but the values fail validation rules. Both are legitimate for field validation, so pick one convention and keep it identical across every endpoint.
The Rule From The Spec
RFC 9110 defines 400 (section 15.5.1) as a request the server cannot or will not process because of something it perceives as a client error, with “malformed request syntax, invalid request message framing, or deceptive request routing” as examples. It is the generic fallback.
422 (section 15.5.21) is narrower: the server understands the content type and the syntax is correct, but it was unable to process the contained instructions. A well-formed body with semantic errors is the textbook case.
So the test has two steps:
- Could the server turn the bytes into a data structure at all? If not, 400.
- If yes, do the values violate a rule (required field missing,
qtyis -3, end date before start date)? 422.
Side By Side
| 400 Bad Request | 422 Unprocessable Content | |
|---|---|---|
| Failure layer | Syntax, framing, parsing | Semantics, validation |
| Typical trigger | Invalid JSON, bad Content-Length, malformed query string, invalid header value | Missing field, wrong type or range, failed business rule |
| Defined in | RFC 9110 section 15.5.1 | RFC 9110 section 15.5.21 (originally WebDAV, RFC 4918) |
| Retry with the same bytes | Will fail again | Will fail again |
| Default for field validation in | Spring MVC, Django REST Framework | Laravel (JSON/XHR), FastAPI, Rails scaffold |
What Popular Frameworks Do By Default
| Framework | Malformed body | Failed validation |
|---|---|---|
| Rails | 400 (ActionDispatch::Http::Parameters::ParseError; ActionController::ParameterMissing for missing required params) | 422 for ActiveRecord::RecordInvalid; the generated scaffold renders status: :unprocessable_entity when save fails |
| Laravel | Not handled by the validator; left to your app | 422 with {"message": ..., "errors": {...}} for JSON/XHR requests. Plain browser form posts get a 302 redirect back with errors flashed to the session |
| FastAPI | 422 (JSON decode errors are reported as json_invalid inside the validation response) | 422 with a detail array |
| Spring MVC / Boot | 400 (HttpMessageNotReadableException) | 400 (MethodArgumentNotValidException from @Valid @RequestBody) |
FastAPI and Laravel behavior is stated in their current docs, and Rails’ 400 mapping is in ActionDispatch::ExceptionWrapper. Rails has been moving from the :unprocessable_entity symbol to :unprocessable_content; the number is still 422. In Spring, ProblemDetail responses change the body shape, not the status.
Third-party APIs split too. The GitHub REST API answers invalid fields with 422 and "message": "Validation Failed" plus an errors array. Stripe answers bad parameters with 400 and type: invalid_request_error.
Which One Should I Use
Request body is not valid JSON. Return 400.
POST /api/orders HTTP/1.1
Host: api.example.com
Content-Type: application/json
{"item_id": 42, "qty": }
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
{"type": "about:blank", "title": "Malformed JSON", "status": 400, "detail": "Unexpected token } at position 24"}
JSON is fine, but a field breaks a rule. Return 422 (or 400 if that is your house convention) and name the field so the client can highlight it.
POST /api/orders HTTP/1.1
Host: api.example.com
Content-Type: application/json
{"item_id": 42, "qty": -1}
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
{"type": "about:blank", "title": "Validation failed", "status": 422,
"errors": [{"field": "qty", "code": "min_value", "message": "qty must be at least 1"}]}
Required query parameter missing on a GET. 400. There is no body to be semantically wrong, and Rails (ParameterMissing) and Spring (MissingServletRequestParameterException) already answer 400.
Valid payload that clashes with existing data (email already registered). 409 Conflict is more informative; fall back to 422 if your clients only handle one validation code.
Wrong Content-Type (sent text/plain, API wants JSON). Neither: 415 Unsupported Media Type.
Valid input, but the caller may not perform the action. Neither: 403 Forbidden.
Common Mistakes
Returning 200 with {"success": false}. Monitoring, retry logic and caches all key off the status line. A failed validation hidden behind 200 cannot be alerted on.
Returning 500 for bad input. An unhandled ValueError or NumberFormatException bubbles up as 500 and pages someone for a typo. Catch parse and validation exceptions at the boundary.
Mixing 400 and 422 in one API. A common real-world pattern is a framework that returns 422 for schema validation while hand-written controllers return 400 for the same kind of failure. Clients end up with two code paths for one concept.
Bare status code with no body. Neither code says which field failed. Return a machine-readable body (RFC 9457 problem details or your own stable shape) with field paths.
Debugging From The Client
curl -i -X POST https://api.example.com/api/orders \
-H 'Content-Type: application/json' \
-d '{"item_id": 42, "qty": }'
If that returns 400 and the same call with "qty": 0 returns 422, the server separates syntax from semantics. When you are unsure which code fits a different failure, the Status Picker tool walks through it.
FAQ
Should validation errors return 400 or 422?
Both are defensible, and RFC 9110 does not pick for you. The strict reading is 422 (section 15.5.21): the body was well-formed and understood, but the instructions inside it cannot be processed. In practice Laravel, FastAPI and the Rails scaffold use 422, while Spring and Django REST Framework default to 400. Consistency across your API matters more than which one you choose.
Is 422 an official HTTP status code or just a WebDAV extension?
It started in WebDAV (RFC 4918), but RFC 9110 section 15.5.21 now defines it for general HTTP use under the reason phrase “Unprocessable Content”. Older docs and frameworks still call it “Unprocessable Entity”; the number is what clients match on.
What status code should malformed JSON return?
- A body that fails to parse (unterminated string, trailing comma) is a syntax problem. Express body-parser, Spring (HttpMessageNotReadableException) and Rails (ParseError) all answer 400. FastAPI is the notable exception: its default handler reports JSON decode failures through the same 422 validation response.
Why does FastAPI return 422 instead of 400?
FastAPI treats every request-validation failure, including type mismatches and missing fields, as a RequestValidationError, and its default handler answers 422 with a “detail” array of error locations. You can change it by registering your own handler for RequestValidationError.
Should a duplicate email on signup be 400, 422 or 409?
Use 409 Conflict when the request is valid but collides with the current state of the resource, such as a unique constraint on email. Use 422 or 400 when the value itself is invalid, such as a malformed address. Many APIs return 422 for duplicates too, but 409 tells clients the same payload could succeed if the state changed.
Do clients treat 400 and 422 differently?
Browsers, proxies and generic HTTP libraries treat both as non-retryable 4xx errors and do not distinguish them. The distinction only matters to your own client code, which is why a stable error body (field names, machine-readable codes) is worth more than the number.