Request bodies with Pydantic, and file uploads
A BaseModel parameter is the request body
A GET request’s only data is its URI. POST/PUT/PATCH can also
send a request body — a separate chunk of data, sent alongside the
request instead of packed into the URI. The curl flags below each do
one job:
-X POST— the HTTP method (curldefaults toGET)-H "Content-Type: application/json"— a header, telling the server the body that follows is JSON-d '...'— the request body itself, attached as literal text-v— verbose: prints the raw request and response, not just the final result, which is what makes the headers/body split visible below
click Run to see this pane's outputEverything after the blank line following the > headers is the
body — plain JSON text, exactly
the same format from the I/O lesson,
just sent over the network instead of read from a file. Content-Type: application/json is what tells the server to interpret it that way.
This is where
Lesson 5’s Pydantic payoff
actually lands: a function parameter typed as a BaseModel tells
FastAPI to parse and validate that JSON body against the model,
automatically, before the route function runs at all.
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class AgentConfig(BaseModel):
name: str
model: str
temperature: float = 0.7
@app.post("/agents")
def create_agent(config: AgentConfig):
return {"created": config.name, "model": config.model}The same request from the demo above calls
create_agent(config=AgentConfig(name="research_agent", model="claude-sonnet"))
— config arrives already validated, already a real AgentConfig
instance with temperature defaulted to 0.7, exactly as
AgentConfig(**raw) worked back in the Pydantic lesson,
just triggered by an incoming HTTP request instead of a
manually-constructed dict. A malformed body — a missing name, a
temperature that can’t be coerced to a float — produces the same
structured 422 response
covered in the previous concept,
automatically, before create_agent ever runs.
Combining a body with path and query parameters
FastAPI figures out where each parameter comes from by its type and
whether it matches a path segment — a BaseModel parameter is
understood as the body, a plain type matching {"{...}"} in the path is
a path parameter, and anything else falls back to a query parameter,
all in the same function signature:
@app.put("/agents/{agent_id}")
def update_agent(agent_id: int, config: AgentConfig, notify: bool = False):
return {"agent_id": agent_id, "updated": config.name, "notify": notify}click Run to see this pane's outputThat single request populates all three parameters from their
respective sources — agent_id from the URI path, config from the
JSON body, notify from the query string — without needing to say so
explicitly anywhere; FastAPI infers each parameter’s source from its
type and its name.
File uploads
A file in a request is a different kind of data than JSON —
UploadFile (paired with File()) is FastAPI’s way of receiving one,
relevant here specifically for a case like a document an agent needs to
process:
from fastapi import FastAPI, File, UploadFile
app = FastAPI()
@app.post("/documents")
async def upload_document(file: UploadFile = File(...)):
contents = await file.read()
return {
"filename": file.filename,
"content_type": file.content_type,
"size_bytes": len(contents),
}click Run to see this pane's outputFile(...) (the ... meaning “required, no default”) marks file as
an uploaded file rather than a JSON body field. UploadFile gives you
.filename, .content_type, and — since reading a file’s contents is
I/O — an async .read() method,
exactly the await-a-coroutine pattern from the async lesson.
This is also why upload_document is declared async def here:
reading an uploaded file’s bytes is genuinely I/O-bound work, the same
category
Lesson 7 built concurrency around.
What happens when a function parameter is typed as a Pydantic BaseModel in a route function?