Reading a 422

Where the validation error comes from, what each part of it means, and how to turn it into something a caller can act on.

Overview

Where it comes from

A 422 response means the server understood your request but refused it because the data was wrong. In FastAPI you will see it constantly, especially while building. A missing field, a string where a number belongs, or a value outside its limits all produce one. The response body explains exactly what went wrong, if you know how to read it.

The error comes from Pydantic, which does the checking, and FastAPI wraps it in a standard JSON shape. Each problem becomes an entry in a list. Each entry says where the problem is, what kind of problem it is, and a human message. Once you know the layout, you can fix most failures in seconds.

This article starts with where the error comes from and why it appears before your handler runs. It then explains the first element of each entry, the location, which tells you whether the problem was in the body, the query, the path, or a header. You will see what an error looks like when many fields fail at once.

It then compares 422 with other errors you might confuse it with, such as 400 and 404, so you can tell which layer is complaining. The later sections show how to reshape the response for your own clients, and how to write tests that confirm bad input is rejected.

Learning to read a 422 quickly is one of the best debugging habits you can build.

Keep a real 422 response next to you as you read, since the examples map directly onto it. Each section is short and ends with a habit you can apply the next time a request fails, so you finish with working knowledge and not only theory.

A 422 isn't a thing FastAPI invents. It's a Pydantic ValidationError, caught at the boundary and rendered as JSON.

The detail array is e.errors() from the Pydantic track, with one addition: loc is prefixed by the part of the request the value came from. So a field error in the body has loc: ["body", "minutes"] rather than just ["minutes"].

all the Pydantic track said about reading these applies right. loc is a path. type is a stable machine code. msg is prose. input is what really arrived. ctx carries the rule's params.

Worth knowing

A 422's detail is Pydantic's e.errors(), with loc prefixed by the part of the request: path, query, header, cookie or body.
Every field is checked, so one response carries every problem — a caller can fix them all in one pass.
422 means the request did not fit the declared shape. It is produced before your handler runs.
404, 403 and 409 are facts your code establishes, so they are HTTPExceptions you raise. Do not conflate them with 422.
An @app.exception_handler(RequestValidationError) lets you return whatever error envelope your clients expect.
In tests, assert on loc and type. Messages are prose and get reworded between releases.

Reading a 422, and Making It Useful

Where the check error comes from, what each part means. How to reshape it for your callers.

It is a Pydantic error in an HTTP envelope

The detail list is really just e.errors(), with one addition: loc starts with the part of the request.

example_01.pyFastAPI
Output

loc says which part of the request

Path, query, header, body — the first element tells the caller where to look, which a bare field name cannot.

example_02.pyFastAPI
Output

Every failure, in one response

Validation does not stop at the first problem, so a caller can fix everything in one pass.

example_03.pyFastAPI
Output

422 is not the same as 400 or 404

Validation produces a 422 automatically. Anything your own code decides is wrong is an HTTPException you raise.

example_04.pyFastAPI
Output

Turning it into your own shape

A custom handler for RequestValidationError lets you return the error format your clients already expect.

example_05.pyFastAPI
Output

Testing that a request is rejected

Assert on loc and type, never on the message — prose gets reworded.

example_06.pyFastAPI
Output

The first element is the useful addition

path, query, header, cookie or body.

That tells a caller *where to look*. This A bare field name can't. A client receiving "minutes is invalid" doesn't know whether to fix the URL, a query param or the JSON they posted. ["query", "limit"] versus ["body", "minutes"] resolves it at once.

It also means code that reads loc[0] as the field name is wrong here in a way it wasn't in plain Pydantic. Skip the first element when you want the field. Use it when you want the source.

Everything at once

check checks each param and each field, then raises once.

For a form, that is the gap between highlighting four broken inputs at once and revealing them one submission at a time. For a machine client, it's one round trip instead of four.

Errors from other sources arrive together, too. A request with a bad path param, a bad query param and two bad body fields makes one response listing all four, each labelled with its source.

422 versus everything else

This gap is worth being precise about. Conflating the codes makes an API harder to use.

422 means the request didn't fit the declared shape. It's made on its own, before your handler runs. It's always the caller's mistake.

400 means the request was syntactically fine but wrong in some way your own code found. It's one you raise.

404 means the thing isn't there. That is a fact about the world, not about the request. /modules/99 is a just right well-formed request. Your code looks, doesn't find, and raises.

409 means the request conflicts with current state: a copy, a version mismatch, an already-done action.

403 means understood, well-formed, and not allowed.

The rule: if check can find it from the declared types and constraints, it's a 422 and you don't write it. If it needs knowing a thing about the world. Existence, permission, current state. It's an HTTPException you raise.

A common mistake is returning 422 for a missing resource because "the id was invalid". The id was fine. Nothing with that id exists. That is a 404.

Reshaping it

The default envelope is a list of objects with loc, type, msg and input. It's precise and it isn't what most front ends want. This Is often a map of field to messages.

An exception handler converts it:

@app.exception_handler(RequestValidationError)
async def tidy(request, exc):
    problems = {}
    for err in exc.errors():
        field = ".".join(str(p) for p in err["loc"][1:]) or "_body"
        problems.setdefault(field, []).append(err["msg"])
    return JSONResponse(status_code=422, content={"errors": problems})

A list per field, because one field can break a few rules at once. The or "_body" catches model-level errors from cross-field checkers, whose loc is just ["body"]. And a thing has to display those. They aren't attached to any input.

Combine it with the message table from the Pydantic track and you get copy a person can read, driven by type rather than by matching on prose, with ctx filling in the actual limits.

Two cautions. Keep the status at 422 rather than inventing your own. Clients and tooling recognise it. And log the first exc.errors() even while returning your tidied version. When a caller says "it rejected my email and it was fine", the input value settles it.

Testing rejections

check deserves tests, and the handy ones assert on loc and type:

expected = {(("body", "title"), "string_too_short"),
            (("body", "minutes"), "greater_than")}

Asserting on msg makes the suite fail when Pydantic rewords a thing. This Teaches people to distrust it. Asserting on the pair tests what you really care about. That the right rule fired on the right field.

Testing the *plus* case matters too. A test that a valid payload gives 201 and the coerced values you expect docs an intention that is else invisible.

What a good error experience looks like

Three things, and they're cheap.

Constrain in the model, so the rule appears in the schema. A caller reading your docs learns the limit before sending anything.

Give fields descriptions and examples. So The interactive docs show a working request rather than an empty box.

**Translate type codes** into sentences your users can act on, falling back to msg for anything unmapped so nothing renders blank.

Most APIs do none of these and return the raw envelope. It's usable. The gap between usable and good here is about twenty lines.

Other errors FastAPI produces

check isn't the only auto failure, and recognising the others saves time.

405 Method Not Allowed means the path matched a route registered for a other method. often a POST to a GET-only route, or a typo in the decorator.

307 Temporary Redirect is the trailing-slash redirect. Harmless until a client drops the body following it. This Turns into a POST that arrives mysteriously empty.

500 with a output message is the response model rejecting what your handler made.

422 on a thing you thought was a query param often means FastAPI classified it as a body. A list annotation without Query() is the common cause.

Handling everything else

RequestValidationError covers input. Two more handlers full the picture.

HTTPException has a default handler you can replace, if you want your own envelope for the 404s and 403s you raise and for check.

A handler for Exception catches all surprise. This Is where you turn an unhandled error into a clean 500 rather than a traceback. Log the real exception there, return a thing plain to the caller. Never include the traceback in the response. It's an data leak. It's the sort that ends up in a screenshot.

Custom exception classes with their own handlers are worth it once an app has real domain errors. Raising ModuleNotFound from a service and mapping it to a 404 in one handler keeps HTTP concerns out of your business logic fully.

Why 422 rather than 400

Some APIs use 400 for check failures. People at times want to change FastAPI's default.

422 means "I understood the request and can't process the entity". The syntax was fine, the content wasn't. 400 means the request itself was malformed. For a well-formed JSON body with a field out of range, 422 is the more precise statement.

The hands-on arg for leaving it alone is that clients and tooling recognise the FastAPI rule. Changing it gains nothing except matching a preference. If you must, an exception handler can return 400 with the same body. But do it always across each endpoint or you've made things worse.

What to log

Log the full exc.errors() with a request identifier, and return your tidied version.

The reason is a conversation that happens with each API. A caller reports that a valid request was rejected. The input field settles it in seconds. It shows just what arrived. This Is often not what they believe they sent. Without it, you're asking them to copy a thing they can't see either.

Don't log the whole body indiscriminately. It may hold passwords, tokens or personal data. A log is a place things persist. The error entries carry the offending values only. This Is often the right amount.

Reading a 422: summary

A 422 is a Pydantic error with an HTTP envelope. loc names the source and the path. type is the stable code to match on. input is what really arrived.

each problem arrives at once. check failures are 422s you never write. Facts about the world. Missing, forbidden, conflicting. Are exceptions you raise. Reshape the envelope with an exception handler if your clients expect a thing else, keep the status, and log the first.

Mistakes people make with Reading a 422

**Reading loc[0] as the field name.** Here the first element is the source. body, query, path, header. The field starts at index one.

Returning 422 for a missing resource. The request was well-formed. Nothing with that id exists. That is a 404 you raise.

**Asserting on msg in tests.** Prose gets reworded and the suite fails for no real reason. This Teaches people to skip it. Assert on loc and type.

**given loc always has a field.** A cross-field checker makes ["body"] with nothing after it. Code indexing past the end raises the first time such a rule is added.

Returning the traceback on a 500. An data leak. The sort that ends up in a screenshot in a public issue.

Not logging the first errors. When a caller insists their request was valid, the input value settles it in seconds. Without it, you're both guessing.

Silencing check errors. Catching them and substituting defaults means the caller sent a thing wrong and will never find out.

Where to go after Reading a 422

That is the foundation: routing, the three sources of input, the shape of the response. What happens when a thing doesn't fit. The next tier goes deeper into the request. Methods, headers, cookies, forms, files, status codes, error handling. Splitting an app into routers.

What the tier covered

Seven modules: what the framework really is, how a function turns a route, the three places input comes from, what the response declares. What happens when a thing doesn't fit.

That is enough to build a real API. all past it's refinement. Better organisation, more of the request, dependencies, the runtime. The practices that keep an use maintainable once it has more than a handful of endpoints.

The next tier goes deeper into the request itself. The methods and what each promises, headers and cookies, form data and file uploads, status codes, error handling. Splitting a growing app into routers before it turns one very long file.

One habit to take away

Read the errors your own API makes before anybody else has to.

Send a on purpose broken request to each endpoint and look at what comes back. Is the message a thing a caller could act on? Does the loc point at the right thing? Is a cross-field rule making an error with nowhere to display it? Is anything sensitive echoed back in input?

It takes minutes per endpoint and it's the only way to see your API the way somebody failing to use it does. Almost each API has at least one error that makes perfect sense to its author and none at all to anyone else.

The shape of a good failure

each rejection an API makes answers three questions. The default envelope answers all three. What was wrong. Here It was, and what was received.

Most hand-rolled check answers one. That gap is the arg for letting the framework give these rather than writing checks in handlers. Not that the code is shorter, though it's. That the caller is told enough to fix the problem without asking anyone.

what envelope you settle on, keep those three. A friendlier message that drops the location has made things worse.

Questions people ask about Reading a 422

What does a 422 mean? The request was well formed but failed validation. Your handler did not run, and the body lists what was wrong.

What is the loc field in the error? It is the path to the bad value: the first element is where it came from (body, query, path, header) and the rest is the field name.

What is the difference between msg and type? msg is a human-readable sentence, while type is a stable machine code such as missing or int_parsing that clients can branch on.

Does FastAPI report one error or all of them? All of them. It validates every parameter and returns the full list in one response.

How do I change the shape of the error response? Register an exception handler for RequestValidationError and return your own JSON.

Why 422 and not 400? 422 Unprocessable Entity says the syntax was fine but the content was semantically invalid. FastAPI uses it for every validation failure.

A closing thought

Errors are the part of an API that gets the least design focus and the most use by anyone struggling with it.

A caller who succeeds first time never reads one. A caller who doesn't is reading nothing else. What they find there decides whether they work it out in a minute or give up and open a ticket. It's worth twenty lines.

Check yourself

0 of 4

Answer without scrolling back up.

  1. What is the first element of `loc` in a FastAPI validation error?

  2. A request for `/modules/99` is well-formed but no module 99 exists. What should it return?

  3. Why assert on `type` rather than `msg` in a test?

  4. How do you return a different error envelope for validation failures?

Cheat sheet

Reading a 422

Read a 422 validation error field by field, trace it back to the Pydantic model, and reshape the response into something callers can use.

FASTAPI · vizlearn.in/fastapi/reading_a_422.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.