Request Body & response_model

RoadmapsPython Backend

Scenario

A client sends `{ "user_id": "abc", "email": "not-an-email" }` to your API. You could write 15 lines of `if type(...) != int:` and regex checks, or you could let the framework instantly reject it with a 422 Unprocessable Entity, detailing exactly which fields failed.

How do you declare the exact shape and types your endpoint expects, so invalid data never even reaches your code?

Mental model

A Pydantic model is a strict bouncer at the door of your endpoint, and `response_model` is the censor on the way out.

In FastAPI, you define a Python class (a Pydantic model) that describes the exact structure of the JSON payload. FastAPI uses this model as a bouncer: if incoming JSON doesn't match the model, it bounces the request with an error before your code runs. If it passes, your code receives a fully instantiated Python object. On the way out, `response_model` acts as a censor: it strips away any fields you don't want to expose (like a password hash) before sending the response.

Deep dive

In FastAPI, you don't manually parse raw request bytes. Instead, you define a Python class inheriting from `BaseModel` (provided by Pydantic) and declare the fields with type hints. When you add this model as a parameter to your route handler, FastAPI knows to read the request body, parse the JSON, and validate it against your model. This is deserialization.

Request bodies are defined by passing a Pydantic model as a function parameter.

Because FastAPI uses these type hints, your editor gets full autocompletion for the request data. You access fields with dot notation (`user.email`), not dictionary keys (`user['email']`). If a client sends a string where an integer is expected, FastAPI automatically attempts to coerce it. If coercion fails, it returns a 422 error response.

Models provide autocompletion and automatic type coercion/validation.

Just as you validate data coming IN, you must control data going OUT. If your endpoint returns a database object that includes a `hashed_password`, returning it directly leaks a secret. By setting the `response_model` parameter in the `@app.post()` decorator, FastAPI guarantees that only the fields defined in the response model are serialized and sent to the client.

Use response_model to filter outbound data and prevent accidental secret leakage.

Under the hood, Pydantic v2 powers this entirely with fast, Rust-backed validation. We'll look at custom validation logic and ORM integration in later lessons, but the core mechanics of the request and response lifecycle remain exactly the same.

Pydantic v2 drives both the inbound validation and outbound serialization.

Code examples

Defining a Request Body

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

# 1. Define the shape of the data
class ItemCreate(BaseModel):
    name: str
    description: str | None = None  # Optional field
    price: float

# 2. Add it as a parameter to the handler
@app.post("/items/")
async def create_item(item: ItemCreate):
    # 'item' is a fully validated Python object, not a dict
    total = item.price * 1.2  # Safe: price is guaranteed to be a float
    return {"name": item.name, "total": total}

By declaring `item: ItemCreate`, FastAPI reads the request body as JSON, validates it against the `ItemCreate` model, and injects the instantiated object into the function.

Filtering outputs with response_model

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class UserIn(BaseModel):
    username: str
    password: str

# Define the safe shape of the output
class UserOut(BaseModel):
    username: str

# The response_model parameter dictates the output shape
@app.post("/users/", response_model=UserOut)
async def create_user(user: UserIn):
    # Simulate saving to DB and returning the raw dict with the password
    saved_user = {"username": user.username, "password": user.password}
    
    # FastAPI will automatically strip the 'password' field before responding
    # because it is not present in UserOut.
    return saved_user

Even though the handler returns a dictionary containing the `password`, FastAPI passes it through `UserOut` which acts as a filter, ensuring the password never hits the network.

Key points

Common mistakes

Glossary

serialization
The process of converting complex Python objects (like Pydantic models) into a format that can be sent over the network, typically JSON.
deserialization
The reverse of serialization: parsing incoming raw bytes (like a JSON string) and validating them into native Python objects.

Recall questions

Questions & answers

A code review flags that an endpoint returns a complete user dictionary retrieved from the database, including their `hashed_password`. How do you fix this leak using FastAPI's built-in features?

Define a Pydantic model containing only the safe fields to expose (e.g., `UserPublic`), and pass it to the route decorator as `response_model=UserPublic`. FastAPI will automatically filter out any fields (like `hashed_password`) that aren't in `UserPublic`.

Approach: Identify `response_model` as the native way to censor output payloads. Returning safe dicts manually is error-prone.

Your endpoint accepts `def update_item(item: ItemUpdate):`. A developer tries to save it by calling `db.save(**item)` but gets a `TypeError`. Why, and how do you fix it?

The injected `item` is a Pydantic object, not a dictionary, so `**` unpacking fails. Fix it by converting the model to a dictionary using `item.model_dump()`.

Approach: Recognize that FastAPI provides fully instantiated class instances, not raw parsed JSON dicts.

Continue learning

Previous: Path & Query Parameters

Next: Pydantic Models and Validators

Return to Python Backend Roadmap