[FastAPI Series 4] Handle Request Bodies with Pydantic

한국어 버전

Path and query parameters cannot carry structured payloads. Request bodies solve that by letting you send JSON with many fields at once. Pydantic—the validation library FastAPI uses internally—reads Python type hints and enforces shape and constraints. After part 3 taught URL patterns (/items/{item_id} vs. ?q=...), we now let clients send the entire item description in one go and validate it via Pydantic models.

Key terms

  1. Request body – the JSON payload carried by POST/PUT requests; the focus of this post.
  2. Pydantic – the parsing and validation engine powered by Python type hints.
  3. BaseModel – the parent class every Pydantic model inherits from to gain validation features.

Practice card

  • Estimated time: 40 minutes
  • Prereqs: Part 3 code, Python 3.12, experience running uv run fastapi dev
  • Goal: pair a Pydantic model with a request body and observe the validation flow

Prefer uvicorn main:app --reload if the fastapi dev CLI is unavailable; both commands reach the same /docs page.

Understanding JSON request bodies

  • Define request schemas with BaseModel
  • Receive JSON and watch FastAPI convert it automatically
  • Inspect 422 responses when validation fails

Build a Pydantic model

FastAPI ships with Pydantic support by default. Declare a model and pass it as a parameter to your endpoint.

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    price: float
    in_stock: bool = True
    tags: list[str] = []

Pydantic makes it safe to use list/dict defaults (it copies them per request), but plain Python classes usually need `default_factory`. Type hints communicate the expected shape to both FastAPI and the generated OpenAPI schema.

Integrating Pydantic models with FastAPI

Add the model to the route signature and FastAPI handles parsing plus validation.

@app.post("/items", status_code=201)
def create_item(item: Item):
    return {"message": "created", "data": item}

FastAPI serializes the returned Pydantic model automatically, so you can return `item`, `item.dict()`, or wrap it inside another object. Here we include a simple status message plus the validated payload.

Swagger UI auto-populates an example JSON payload and displays the field descriptions.

Example: counseling request intake

For a student counseling app, define the payload like this:

class CounselingRequest(BaseModel):
    student_id: int
    topic: str
    preferred_slots: list[str]

@app.post("/counseling")
def create_request(payload: CounselingRequest):
    return {"status": "queued", "student_id": payload.student_id}

Frontends built in any language can inspect the autogenerated OpenAPI schema (or the /docs page) to learn the exact JSON structure, because FastAPI exports every Pydantic model as part of its spec.

Test with uv and curl

uv run fastapi dev main.py

Then send a request:

curl -X POST http://127.0.0.1:8000/items \
  -H "Content-Type: application/json" \
  -d '{"name": "notebook", "price": 3.5, "tags": ["study"]}'

If fastapi dev is missing, use uv run uvicorn main:app --reload. Either way, visit http://127.0.0.1:8000/docs to send the same request through Swagger UI if you prefer clicking over curl. The response body mirrors the model structure shown in your Pydantic class.

Validation and error handling

Missing a required field or sending the wrong type triggers a 422.

{
  "detail": [
    {
      "type": "missing",
      "loc": ["body", "item", "name"],
      "msg": "Field required"
    }
  ]
}

Swagger shows the same diagnostic message.

422 Unprocessable Entity means FastAPI parsed the JSON successfully but could not validate it against the schema. Pydantic will also try to coerce reasonable inputs (for example, the string "5" into the integer 5); add stricter validators or Strict* field types if you want to forbid coercion.

Nested models and defaults

Pydantic handles nested objects naturally.

class Supplier(BaseModel):
    name: str
    email: str

class Item(BaseModel):
    name: str
    price: float
    supplier: Supplier | None = None

Send supplier in the request body to receive a structured object; omit it to have FastAPI set None automatically. On Python 3.9 or earlier, write Optional[Supplier] = None instead of the Supplier | None union syntax.

Practice

  • Follow along: start the dev server, open /docs, create Item, POST to /items, and confirm the 201 response.
  • Extend: add the Supplier nested model and modify the Swagger example JSON to include { "supplier": { "name": "Stationery Co", "email": "[email protected]" } }.
  • Debug: remove a required field or send the wrong type to read the 422 payload.
  • Done when: both success and failure cases behave exactly as expected and you can describe the schema verbally.

Wrap-up

To accept structured data, declare the schema with Pydantic and let FastAPI parse plus validate it for you. Next time we will reuse these models to build a small CRUD flow backed by an in-memory list.

💬 댓글

이 글에 대한 의견을 남겨주세요