SkillByAIOpen interactive version →

Lesson 4 / 26

Request and Response Models With Pydantic

Validate in, filter out.

BaseModel, Field, response_model

Request bodies are Pydantic models: fields with types and constraints (Field(min_length=2), gt=0) are validated before your code runs, and invalid input returns 422 with every problem listed. A response_model (or return type annotation) filters and serialises the output, so internal fields such as costs or password hashes never leak. Set status_code=201 for creation endpoints. Keep separate models for input and output.

Models, dependencies, async, docs

Pydantic models validate data, dependencies share logic, async handles I/O concurrency, and OpenAPI documents it all.

Figure 2.1 — Models, dependencies, async and docs.

Validation and output filtering, run

I ran this with Python 3.12, FastAPI 0.142.2, Pydantic 2.13 and Starlette 1.7, calling the app through FastAPI's TestClient (no server needed). A valid product returns 201 and the internal_cost field is stripped by the response model. An invalid body returns 422 listing both the too-short name and the non-positive price.

from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field

class ProductIn(BaseModel):
    name: str = Field(min_length=2, max_length=50)
    price: float = Field(gt=0)
    tags: list[str] = []

class ProductOut(BaseModel):
    id: int
    name: str
    price: float

app = FastAPI()
DB: dict[int, dict] = {}

@app.post("/products", response_model=ProductOut, status_code=201)
def create(p: ProductIn):
    pid = len(DB) + 1
    DB[pid] = {"id": pid, **p.model_dump(), "internal_cost": 42}   # extra field
    return DB[pid]                       # response_model filters the output

client = TestClient(app)
r = client.post("/products", json={"name": "Pen", "price": 899, "tags": ["office"]})
print(r.status_code, r.json())
r = client.post("/products", json={"name": "X", "price": -5})
print(r.status_code, [(e["loc"][-1], e["msg"]) for e in r.json()["detail"]])

Output:

201 {'id': 1, 'name': 'Pen', 'price': 899.0}
422 [('name', 'String should have at least 2 characters'), ('price', 'Input should be greater than 0')]

Separate input and output models

ProductIn, ProductOut and ProductDB models make it obvious what clients can send and what they receive.

Quick check: What does response_model do with fields not in the model?

  • Adds them as strings
  • Removes them from the response
  • Raises an error
  • Encrypts them
Answer

Removes them from the response — Output filtering prevents leaks.