Evaluating CodeRabbit? Same review, plus real test runs. See why

API Testing8 min read

HTTP 422 Unprocessable Content: Meaning, Causes, and Fixes

S
Technical Writer, Qodex
The number 422 in large bold type with Unprocessable Content under it, centered on a light background

HTTP 422 Unprocessable Content means the server understood the request format and could parse it, but the content fails validation: valid JSON with an invalid email, a quantity of zero, or an end date before the start date. Malformed JSON is 400, not 422. Fix the data, then resend.

422 is one of the 4xx HTTP status codes, the group that puts the fault on the request. The payload arrived intact and the server read it. A rule inside it did not hold. This page covers the definition, the response shape, the codes people confuse it with, the framework fixes, and a test that stays stable.

What the RFC says about 422

RFC 9110 section 15.5.21 puts it this way: "The 422 (Unprocessable Content) status code indicates that the server understands the content type of the request content ... unable to process the contained instructions." The part elided in the middle says the syntax of the content is correct.

In plain terms: parseable, in a format the server supports, and still rejected, because a value or an instruction fails a rule.

The name changed along the way. RFC 9110 calls the code Unprocessable Content. The older reason phrase, Unprocessable Entity, still turns up in libraries and in most search results. Spring is mid-migration: unprocessableEntity() is deprecated in Framework 7 in favor of unprocessableContent(), per the ResponseEntity Javadoc. Both names mean the same code.

What a 422 response must carry

No header is required for a 422 specifically. Content-Type describes the response body, as it always does. Cache-Control: no-store is a policy choice for when field errors echo sensitive submitted values, not a rule of the status code (RFC 9111 section 3). A 422 is not heuristically cacheable, so a cache may store it only when explicit controls such as max-age, s-maxage, or Expires say so.

Use Content-Type: application/problem+json. That is problem details, the JSON error format defined in RFC 9457, which even gives a 422 validation example with an errors extension. The members: type identifies the class of problem, title is a short summary, status is optional but must match the HTTP status when present, detail describes this occurrence for a human, and instance identifies the occurrence itself. The errors array, the pointers, and the messages inside it are extensions you define.

POST /orders HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/problem+json

{"email": "not-an-email", "quantity": 0}

HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
Cache-Control: no-store

{
  "type": "https://api.example.com/problems/validation-error",
  "title": "Request validation failed",
  "status": 422,
  "detail": "Two fields failed validation.",
  "instance": "/problems/req-7f3c",
  "errors": [
    { "pointer": "#/email", "detail": "must be a valid email address" },
    { "pointer": "#/quantity", "detail": "must be a positive integer" }
  ]
}

The pointers are what make this useful to a client. A form can highlight two fields without any string matching on detail.

400 vs 422 vs 415 vs 409

CodeUse it whenExampleSource
400 Bad RequestMalformed syntax, invalid framing, deceptive routing, or another broad client errorTruncated JSONRFC 9110 section 15.5.1
415 Unsupported Media TypeThe media type or content encoding is not supported for this resource and methodXML sent to a JSON-only endpointRFC 9110 section 15.5.16
422 Unprocessable ContentFormat understood, syntax valid, instructions cannot be processedValid JSON with a quantity of 0 where the minimum is 1RFC 9110 section 15.5.21
409 ConflictThe request conflicts with the current state of the resource, and the client may fix the state and resubmitA stale version overwriting a newer revisionRFC 9110 section 15.5.10

One correction worth stating outright, because a page-one result gets it wrong. A wrong or unsupported Content-Type is 415, not 422. Postman's 422 guide lists it as a 422 cause. RFC 9110 section 15.5.16 gives it its own code.

What causes a 422 and how to fix it

CauseBad inputFix
Missing required fieldValid JSON without author_idSend every required field. The schema, not the example in the docs, is the source of truth.
Wrong JSON typeA price of "29.99" as a string where a number is requiredSend 29.99. Validate types before the request leaves the client.
Invalid formatAn email with no domain, a date with month 13Match the documented email, date, URL, or ID format.
Range or enum violationA quantity of 0, a priority outside the allowed setEnforce the same minimum, maximum, and enum rules on both sides.
Cross-field or business ruleAn end date before the start dateReturn field pointers so the client knows which pair broke, then fix the relationship.
Contract driftAn old field name still being sent after a schema changeRegenerate the client from the current schema and keep a regression test for the payload that was rejected.

Most of those rows come from the Postman guide. Two things are not 422 causes, whatever a stack trace suggests: malformed JSON is 400, and an unsupported format is 415.

Express

Express does not choose 422 on its own. With express-validator, validation chains record their errors and validationResult(req) reads them back. Keep JSON parse failures on the 400 path. The branch below runs only after express.json() has already parsed a supported body.

app.post(
  '/orders',
  body('email').isEmail(),
  body('quantity').isInt({ min: 1 }),
  (req, res) => {
    const result = validationResult(req);
    if (result.isEmpty()) return createOrder(req, res);

    return res.status(422).type('application/problem+json').json({
      type: 'https://api.example.com/problems/validation-error',
      title: 'Request validation failed',
      status: 422,
      errors: result.array().map(({ path, msg }) => ({
        pointer: '#/' + path,
        detail: msg,
      })),
    });
  },
);

FastAPI

FastAPI reads the typed model you declared, converts compatible values, validates the rest, and reports where the data failed. Its documented error example serializes a RequestValidationError with status 422. If size is declared as an int and the request sends the string "XL", the client fix is to send an integer, or to change the contract if strings are genuinely valid. For a public API, add a RequestValidationError handler that translates the default response into problem+json while keeping the field locations intact.

Spring MVC

@Valid on a @RequestBody raises MethodArgumentNotValidException. Map that to 422 with a ProblemDetail body and leave unreadable JSON on the 400 path. The API names below are Framework 7. Before that release they were HttpStatus.UNPROCESSABLE_ENTITY and ResponseEntity.unprocessableEntity(), which still work and are still what most existing code uses.

@ExceptionHandler(MethodArgumentNotValidException.class)
ResponseEntity<ProblemDetail> invalidBody(MethodArgumentNotValidException ex) {
    ProblemDetail problem = ProblemDetail.forStatusAndDetail(
        HttpStatus.UNPROCESSABLE_CONTENT,
        "One or more fields failed validation"
    );
    return ResponseEntity.unprocessableContent().body(problem);
}

NGINX

This is the gateway case, where the origin returns a correct 422 and NGINX replaces it with something else. proxy_intercept_errors sends any proxied response with a status of 300 or higher through error_page. The default is off, and off is usually what you want for an API. If you do need an error page for 422, configure it to preserve the status and the body. This fixes propagation. It is not a reason for the origin to emit a 422.

location /api/ {
    proxy_pass http://api_backend;
    proxy_intercept_errors off;
}

Should a client retry a 422?

Not unchanged. MDN puts it plainly: an unchanged repeat should be expected to fail again. Retry-After has defined uses for 503 and for redirects (RFC 9110 section 10.2.3), not for a 422, and no amount of backoff repairs invalid content.

What a client should do instead: read type and the structured errors, map each pointer to a field, let the caller or the user correct the data, and resubmit. Do not parse detail as prose, and do not silently coerce the value into something that passes.

A connection failure before any response arrives is a different question entirely. There the relevant property is whether the method is idempotent, meaning a repeated call leaves the same result as a single one. GET, PUT, and DELETE are; POST is not guaranteed to be. HTTP methods covers which is which.

How to test a 422 response

Send syntactically valid JSON that breaks one known rule, then assert four things: the status, the media type, the problem type, and one stable field pointer. A status assertion on its own can pass for the wrong reason, because plenty of bugs produce a 422 by accident. The rest of API testing applies as usual.

import { test, expect } from '@playwright/test';

test('rejects a semantically invalid order with 422', async ({ request }) => {
  const response = await request.post('/orders', {
    headers: {
      'Content-Type': 'application/json',
      Accept: 'application/problem+json',
    },
    data: { email: 'not-an-email', quantity: 0 },
  });

  expect(response.status()).toBe(422);
  expect(response.headers()['content-type']).toContain('application/problem+json');

  const problem = await response.json();
  expect(problem).toMatchObject({
    status: 422,
    type: 'https://api.example.com/problems/validation-error',
  });
  expect(problem.errors).toEqual(
    expect.arrayContaining([expect.objectContaining({ pointer: '#/quantity' })]),
  );
});

The flaky version of this test uses a shared email address and expects duplicate registration to return 422, without first creating or resetting that user. It returns 201 when the user is absent and 422 when a previous run left one behind, so the result depends on run order. Fix it with an explicit prerequisite, per-run data, and a deterministic invalid field when the test is about validation rather than database state. Avoiding flaky tests here mostly means not asserting only that the status equals 422.

How Qodex flags a 422

Qodex generates API tests from an OpenAPI spec, a Postman collection, or a chat brief, and it writes negative cases alongside the happy paths. Those include the out-of-range and wrong-type payloads that should return 422. They run on every pull request, and an endpoint that answers 200 or 500 where the contract says 422 fails with the exact payload attached, classified as a real bug rather than a stale test. See how Qodex API testing works.

Frequently Asked Questions

What does the 422 status code mean?

The server understood the request format and its syntax, but cannot process the content because a value breaks a validation or business rule. RFC 9110 section 15.5.21 defines it that way.

What is the difference between 400 and 422?

400 covers malformed syntax, such as truncated or broken JSON that the server cannot parse at all. 422 is well-formed content that fails a rule, such as an invalid email inside otherwise valid JSON.

What is the difference between 409 and 422?

409 is a conflict with the resource's current state, such as a stale version overwriting a newer one. 422 is invalid content, and it stays invalid no matter what state the resource is in.

What is the difference between 415 and 422?

415 means the media type or content encoding is unsupported, for example XML sent to a JSON-only endpoint. 422 means the type was fine and the values inside the body were not.

Should a client retry a 422 response?

Not unchanged. Read the field errors, correct the data, and send a new request. Backoff and retries do not repair invalid content, so an automatic retry just earns the same rejection again.

Is a 422 response cacheable?

Not by default. It is not on the heuristically cacheable list, so a cache may store it only when the response carries explicit directives such as max-age, s-maxage, or Expires.

What headers and JSON fields should a 422 response include?

Content-Type application/problem+json, with type, title, status, detail, and an errors array of field pointers. Add Cache-Control no-store when the error messages echo values you would rather no proxy stored.

Why does FastAPI return 422 for invalid request data?

FastAPI validates every request against the model you declared and, by default, serializes a RequestValidationError with status 422. The response names the location of each field that failed, which is why the code appears so often.

Ship continuously. Test continuously.

Qodex explores your app, writes runnable tests, and replays them on every change at zero LLM cost.