Add Response Models
Create ExpenseOut and ErrorResponse models and attach them to your endpoints
Writing code and entering commands is only available on desktop. Open this page on a larger screen to complete this chapter.
Defining the response shape
Your stored expense dictionaries have five fields: id, description, amount, category, and date. The ExpenseOut model mirrors this shape as a Pydantic class. FastAPI uses it to validate outgoing data and generate documentation.
You already have an Expense model for input validation. ExpenseOut is separate because the response includes an id field that the client never sends — the server generates it.
The ErrorResponse model
Error responses from HTTPException always have a detail field. Defining an ErrorResponse model with detail: str gives FastAPI a schema to document these errors on the /docs page.
Attaching a response model
Add response_model=ExpenseOut to a decorator to tell FastAPI the response shape for that endpoint:
@app.post("/expenses", status_code=201, response_model=ExpenseOut)For endpoints that return a list, wrap the model in list[]:
@app.get("/expenses", response_model=list[ExpenseOut])
Instructions
Add two new model classes after ExpenseUpdate and before save_expenses. Then attach response models to the create and list endpoints.
- Create a class named
ExpenseOutthat inherits fromBaseModelwith five fields:idasint,descriptionasstr,amountasfloat,categoryasstr, anddateasOptional[str]with a default ofNone. - Create a class named
ErrorResponsethat inherits fromBaseModelwith one field:detailasstr. - Add
response_model=ExpenseOutto the@app.post("/expenses", status_code=201)decorator. - Add
response_model=list[ExpenseOut]to the@app.get("/expenses")decorator.
import json
from datetime import datetime
from pathlib import Path
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field, field_validator
from typing import Literal, Optional
app = FastAPI()
DATA_FILE = Path("expenses.json")
class Expense(BaseModel):
description: str = Field(min_length=1)
amount: float = Field(gt=0)
category: Literal["food", "transport", "entertainment", "utilities", "other"]
date: Optional[str] = None
@field_validator("date")
@classmethod
def validate_date_format(cls, v):
if v is not None:
try:
datetime.strptime(v, "%Y-%m-%d")
except ValueError:
raise ValueError("Date must be in YYYY-MM-DD format")
return v
class ExpenseUpdate(BaseModel):
description: Optional[str] = Field(default=None, min_length=1)
amount: Optional[float] = Field(default=None, gt=0)
category: Optional[Literal["food", "transport", "entertainment", "utilities", "other"]] = None
# Step 1: Create ExpenseOut(BaseModel) with id: int, description: str, amount: float, category: str, date: Optional[str] = None
# Step 2: Create ErrorResponse(BaseModel) with detail: str
def save_expenses():
data = {"counter": counter, "expenses": expenses}
DATA_FILE.write_text(json.dumps(data, indent=2))
def load_expenses():
global expenses, counter
if DATA_FILE.exists():
data = json.loads(DATA_FILE.read_text())
expenses = {int(k): v for k, v in data["expenses"].items()}
counter = data["counter"]
expenses = {}
counter = 0
load_expenses()
# Step 3: Add response_model=ExpenseOut to this decorator
@app.post("/expenses", status_code=201)
def create_expense(expense: Expense):
global counter
counter += 1
expenses[counter] = {"id": counter, **expense.model_dump()}
save_expenses()
return expenses[counter]
# Step 4: Add response_model=list[ExpenseOut] to this decorator
@app.get("/expenses")
def list_expenses(category: str = None):
if category:
return [e for e in expenses.values() if e["category"] == category]
return list(expenses.values())
@app.get("/expenses/{expense_id}")
def get_expense(expense_id: int):
if expense_id not in expenses:
raise HTTPException(status_code=404, detail="Expense not found")
return expenses[expense_id]
@app.delete("/expenses/{expense_id}")
def delete_expense(expense_id: int):
if expense_id not in expenses:
raise HTTPException(status_code=404, detail="Expense not found")
save_expenses()
return expenses.pop(expense_id)
@app.patch("/expenses/{expense_id}")
def update_expense(expense_id: int, updates: ExpenseUpdate):
if expense_id not in expenses:
raise HTTPException(status_code=404, detail="Expense not found")
expenses[expense_id].update(updates.model_dump(exclude_unset=True))
save_expenses()
return expenses[expense_id]
Interactive Code Editor
Sign in to write and run code, track your progress, and unlock all chapters.
Sign In to Start Coding