A parameter annotated with a Pydantic model is the request body. FastAPI reads the raw bytes, validates them with model_validate_json, and calls your handler with an object.
By the time the function runs, module.minutes is an integer — even if the client sent "8" — and every constraint on the model has passed. There is nothing to check at the top of the handler, which is the point.
This is also the moment where the whole Pydantic track becomes directly applicable. Field constraints, nested models, validators, aliases, enums, Literal, strict mode: all of it works here, unchanged, because this *is* Pydantic.
Worth knowing
A parameter annotated with a Pydantic model is the request body. FastAPI parses the JSON and validates it before your handler runs.
The 422's detail is essentially e.errors(), with loc beginning body so a caller knows which part of the request failed.
Nesting works to any depth, and the error path names the exact item: ('body', 'lessons', 1, 'minutes').
A handler can take path, query and body parameters together — FastAPI decides each by where its name appears and what it is annotated as.
Two model parameters are nested under their names in the body rather than merged.
Unknown keys are ignored by default, which hides a typo as a silently-defaulted field. extra="forbid" makes it an error.
Request Bodies: A Model Is the Contract
Everything the Pydantic track taught, applied to an HTTP request.
A model parameter is the body
Annotate a parameter with a model and FastAPI parses the JSON, validates it, and hands you an object.
example_01.pyFastAPI
Output
Failures never reach your code
Every field is checked and the 422 carries the whole list, with loc starting at body.
example_02.pyFastAPI
Output
Nested and repeated structure
A model can contain models and lists of them. The error path reaches all the way down.
example_03.pyFastAPI
Output
Body plus path plus query
One handler can take all three. FastAPI works out where each comes from and the error says which.
example_04.pyFastAPI
Output
Two models in one body
Two model parameters make FastAPI nest them under their names, which is occasionally what you want.
example_05.pyFastAPI
Output
Rejecting keys you did not declare
By default unknown keys are ignored, which hides typos. extra="forbid" turns them into errors.
example_06.pyFastAPI
Output
Failures happen first
If the body does not fit the model, your handler is never called. FastAPI returns 422 with a detail that is essentially e.errors().
The loc begins with body, then names the field, then the path within it. So ("body", "lessons", 1, "minutes") is the minutes field of the second lesson. For a payload of any size that is the whole diagnosis.
Every field is checked, so a client with four mistakes learns about all four at once rather than one resubmission at a time. This matters most for forms, where the alternative is a genuinely unpleasant experience.
Structure
Bodies nest as deeply as your models do. A List[Lesson] inside a Module validates each lesson with its own rules, and errors keep their index.
Two things worth carrying over from the Pydantic track.
Constrain collections.Field(min_length=1) says a module must have at least one lesson; max_length caps how many, which is a cheap protection against a caller sending a hundred thousand.
Extract models that repeat. If Address appears in three request bodies, it is a model, and the rules then live in one place.
Path, query and body together
A handler can take all three, and FastAPI decides which is which by the rules from the previous modules: names in the route path are path parameters, Pydantic models are the body, everything else is a query parameter.
The signature reads as documentation, and the 422 for a bad request names each source separately — ("path", "module_id") and ("body", "minutes") are different problems for the caller to fix.
Two models
If you annotate two parameters with models, FastAPI nests them under their parameter names rather than merging:
{"module": {...}, "author": {...}}
That is occasionally what you want. More often it is a sign that the two belong in one model, because the client now has to know a structure that exists only because of how your function was written. A single model that contains both is usually clearer, and easier to document.
Body(embed=True) forces the same nesting for a single model, when an existing API expects it.
Unknown keys
By default Pydantic ignores keys it does not recognise. A client sending minuets instead of minutes gets no error and no field — minutes takes its default, and the bug shows up later as a duration nobody set.
model_config = ConfigDict(extra="forbid") makes that a 422 naming the offending key.
Which to choose is a real decision. For an internal API, forbid: a typo should be loud. For a public one, the argument for ignoring is forward compatibility, since a client sending fields from a newer version should not break against an older server.
What you should not do is leave it unconsidered, which is what usually happens.
Which methods take a body
POST, PUT and PATCH do. GET and DELETE conventionally do not, and while FastAPI will let you declare a body on a GET, many clients, proxies and caches will drop it. If a GET needs structured input, that is a sign it wants query parameters, or that it is really a POST.
The distinction between the three that do:
POST creates something, and the server decides its identity.
PUT replaces a resource entirely at a known URL — so a PUT body should contain every field, and omitting one means clearing it.
PATCH updates part of one, which is where exclude_unset from the Pydantic track earns its place: dump only what the caller supplied, so untouched fields stay untouched.
Getting PUT and PATCH backwards is common and produces the bug where an update wipes fields the client never mentioned.
Raw bodies
Not everything is JSON. Request gives you the raw object, with await request.body() for the bytes — needed for a webhook whose signature is computed over the exact payload, since re-serialising a parsed model produces different bytes.
Body(media_type="text/plain") handles a plain-text body. Form data and file uploads have their own module in the next tier.
Reach for these deliberately. A raw body means no validation, no schema, and no documentation, so it should be a considered exception rather than a way of avoiding writing a model.
Designing the model
Two habits, both from the Pydantic track and both worth repeating because bodies are where they pay off most.
A separate model per direction.ModuleCreate takes what a caller may supply, with no server-assigned id. ModuleUpdate has everything optional. ModuleOut declares exactly what may be seen. One model with everything optional documents nothing and guarantees nothing.
Describe the fields. A description on a body field appears next to it in the docs, and an examples entry pre-fills the interactive request form — which is the difference between a first-time caller succeeding and guessing.
Documenting the body
Everything the Pydantic track said about schema metadata applies, and bodies are where it pays off most.
description on a field appears beside it in the interactive docs. examples pre-fills the request form, which is the difference between a first-time caller pressing "Try it out" and getting a working request, or an empty box they have to guess at.
Now the docs show a complete sample payload somebody can copy.
Body(embed=True) and Body(examples=[...]) do the same at the parameter level when you need to override what the model says.
Size limits
FastAPI does not cap request body size by default. That is worth knowing, because an endpoint accepting JSON will attempt to parse whatever arrives.
The cap normally belongs upstream — in nginx, in your ingress, in the platform — and it is one of the standard things to check before an API is public. Within the app, max_length on collections limits how many items a body may contain, which handles the common case of a bulk endpoint being handed a hundred thousand records.
Validation that needs the whole body
A rule spanning two fields belongs in a model_validator(mode="after"), exactly as in the Pydantic track. The error arrives with loc: ["body"] — no field, because it belongs to the object — and a client needs somewhere to display it.
A rule spanning the body *and* a path parameter is different: no model can see both. That is a dependency, or a check at the top of the handler raising a 400 or 409. It is one of the few cases where logic in the handler is the honest answer, because the relationship is between HTTP concerns rather than within the data.
Idempotency, briefly
A POST that creates something is not idempotent: a client that times out and retries may create two.
The usual answer is an idempotency key — a header the client generates, which the server records alongside the result, returning the original response on a repeat. That is beyond a first tier, but it is worth knowing the problem exists before an API is handling anything that matters, because retrofitting it after the duplicates appear is much harder.
Summary
A model parameter is the body. Everything from Pydantic applies: constraints, nesting, validators, aliases, strict mode.
Validation runs before your handler, and a 422 carries every problem with loc starting at body. Unknown keys are ignored unless you forbid them. Separate models per direction, and describe the fields — the docs are generated from them, and a good example is the cheapest thing you can do for whoever calls you.
Mistakes people make
Reusing one model for input and output. The result is a model where everything is optional, which documents nothing and guarantees nothing in either direction.
Leaving extra at the default on an internal API. A misspelt key silently becomes a defaulted field, and the bug appears later as a value nobody set.
Treating PUT as PATCH. A PUT body should be complete; an omitted field means clearing it. Using PUT for partial updates is how an edit wipes six columns the client never mentioned.
Declaring a body on a GET. Some clients and proxies drop it. If a read needs structured input, it wants query parameters, or it is really a POST.
Two model parameters where one model belongs. It forces callers to learn a nesting that exists only because of how your function was written.
No cap on collection size.max_length on a list field is what stops a bulk endpoint being handed a hundred thousand records.
Re-serialising a body you needed verbatim. A webhook signature is computed over the exact bytes; await request.body() is the only thing that gives you those.
Next
The other half of the exchange: response_model, which decides what comes back and, just as importantly, what cannot leak.
The body is the contract
Of everything an endpoint declares, the body model is the part consumers read most carefully, because it is what they have to construct.
Which makes it worth more care than the rest. Field names that read well. Descriptions on anything not self-evident. A complete example. Constraints that state the real limits rather than leaving them to be discovered by rejection. Required fields that are genuinely required, and defaults that are genuinely sensible.
None of that changes behaviour. All of it changes how long somebody spends getting their first successful request, which is the number that decides whether they enjoy using your API.
Accepting change from clients
The mirror of the previous point: how a body model can evolve without breaking senders.
Adding an optional field is safe. Old clients omit it and get the default.
Adding a required field is breaking. Every existing client immediately starts getting 422s. If a field must become required, give it a default first, deprecate the absence, then tighten.
Loosening a constraint is safe. Tightening one is not — a max_length reduced from 100 to 50 rejects requests that worked yesterday.
Renaming is breaking, unless you accept both names for a while, which is exactly what AliasChoices from the Pydantic track is for.
This is why extra="ignore" is defensible on a public API even though forbid catches typos: a client sending fields from a newer version keeps working against an older server. It is a trade between catching mistakes and tolerating drift, and which side you want depends on who is calling.
What the model is really doing
A body model is doing three jobs that would otherwise be three separate pieces of code.
It parses, turning bytes into typed values. It validates, rejecting anything that does not fit before your logic sees it. And it documents, appearing in the schema so callers know what to send without reading your source.
Written by hand those drift apart: the parser accepts something the validator rejects, and the documentation describes a shape neither of them implements. One class keeps all three in agreement because they are all generated from it.
A closing thought
Almost every question about request bodies turns out to be a Pydantic question wearing an HTTP hat: what will it coerce, what is required, how do I express this rule, why did that fail.
Which is good news, because it means the body is the part of an API where the least framework-specific knowledge is needed. Get the models right and the endpoints are a formality.
Check yourself
0 of 4
Answer without scrolling back up.
How does FastAPI know a parameter is the request body?
A model annotation is the signal. Path names come from the route, and anything else defaults to a query parameter.
What is `loc` for a bad field inside the second item of a list in the body?
The path names the source, the field, the index and the inner field - which for a large payload is the entire diagnosis.
A client sends `minuets` instead of `minutes`. What happens by default?
Pydantic ignores unknown keys unless told otherwise. `extra="forbid"` makes it a 422 - usually right for an internal API, weighed against forward compatibility for a public one.
What distinguishes PUT from PATCH?
A PUT body should be complete, and an omitted field means clearing it. PATCH updates only what was sent, which is what `model_dump(exclude_unset=True)` is for.
Cheat sheet
Request Bodies
A parameter annotated with a Pydantic model is the request body. FastAPI reads the raw bytes, validates them with model_validate_json, and calls your handler with an object.
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.