Pydantic request and response models
Define the wire contract with Pydantic: validation, custom validators, and response models that stop internal fields leaking out of your API.
Request models
# schemas.py
from pydantic import BaseModel, ConfigDict, Field, field_validator
class NoteIn(BaseModel):
model_config = ConfigDict(extra="forbid") # reject unknown fields
title: str = Field(min_length=1, max_length=120)
body: str = ""
tags: list[str] = Field(default_factory=list, max_length=10)
@field_validator("title")
@classmethod
def normalise_title(cls, value: str) -> str:
value = " ".join(value.split())
if not value:
raise ValueError("title cannot be blank")
return value
@field_validator("tags")
@classmethod
def lowercase_tags(cls, values: list[str]) -> list[str]:
return sorted({t.strip().lower() for t in values if t.strip()})from fastapi import FastAPI
from schemas import NoteIn
app = FastAPI()
@app.post("/notes", status_code=201)
def create_note(note: NoteIn) -> dict[str, object]:
return {"stored": note.title, "tags": note.tags}- Declaring the model as a parameter makes it the JSON body — no
request.json()and no manual parsing. extra="forbid"turns typos in the payload into a 422 instead of silently ignoring them.default_factory=listavoids the shared-mutable-default bug that plain= []would create.- Field constraints (
min_length,ge,pattern) are enforced before your handler runs.
Response models
class NoteOut(BaseModel):
model_config = ConfigDict(from_attributes=True) # build from ORM objects
id: int
title: str
body: str
@app.post("/notes", response_model=NoteOut, status_code=201)
def create_note(note: NoteIn):
row = db_insert(note) # returns an object with id, title, body AND password_hash
return row # response_model strips everything not declared
@app.get("/notes/{note_id}", response_model=NoteOut)
def get_note(note_id: int):
return db_get(note_id)
@app.get("/notes", response_model=list[NoteOut],
response_model_exclude_unset=True)
def list_notes():
return db_list()⚠️
Without
response_model (or a return type annotation) FastAPI serialises whatever you return. Returning an ORM row directly then hands every column — password hashes, internal flags, other users' data — to the client. Declare the output shape explicitly on every endpoint.Useful patterns
class NotePatch(BaseModel):
title: str | None = None
body: str | None = None
@app.patch("/notes/{note_id}", response_model=NoteOut)
def patch_note(note_id: int, patch: NotePatch):
# exclude_unset distinguishes "field absent" from "field set to null"
changes = patch.model_dump(exclude_unset=True)
return db_update(note_id, changes)
class Page(BaseModel):
id: int
title: str
class PagedNoteOut(BaseModel):
items: list[NoteOut]
total: int
next_cursor: str | None = None| Goal | Tool |
|---|---|
| Reject extra keys | ConfigDict(extra="forbid") |
| Read from ORM / objects | ConfigDict(from_attributes=True) |
| Partial updates | model_dump(exclude_unset=True) |
| Hide unset defaults in output | response_model_exclude_unset=True |
| Cross-field checks | @model_validator(mode="after") |
| Reuse validators | Mixin base classes or Annotated types |
FAQ
Where should I put the shared fields?
Split into
NoteBase / NoteIn / NoteOut and inherit. Output models should never carry fields the client is not allowed to set, such as id or created_at.Can I return a different shape on error?
Yes — declare it in the decorator with
responses={404: {"model": ErrorOut}} so the generated OpenAPI schema and the client SDK stay accurate.Related
Your first FastAPI app Dependencies and async
Last refreshed 2026-09-18.