Data Modeling with Pydantic & SQLAlchemy
Food Tracker (FastAPI)
Chapter 2 · Data Modeling with Pydantic & SQLAlchemy
Every course in this quartet stores the same shape of data. This course expresses it as two genuinely separate models — one for what actually lives in the database, one for what a request is allowed to send.
The SQLAlchemy Model: Storage
The same design decisions every course in this quartet already made: expiry_date nullable, cleared rather than the row deleted once an item is used; added_at stamped by the database itself via server_default=func.now(), never trusted from client input.
The Pydantic Schemas: Request & Response Shape
ItemCreate deliberately has no id, no status, and no added_at field at all — not because the client sends them and they're ignored, but because they genuinely don't exist as fields the schema will accept. A request body containing a status field is simply rejected as an unexpected field, before any route handler code runs.
if (!name || typeof name !== "string" || !name.trim()) — inside the route handler's own body, and its own finding-box named that server-side check as the app's real gate. Here, the equivalent gate is the ItemCreate class itself: declaring item: ItemCreate as a route parameter means FastAPI validates the incoming JSON against that schema automatically, rejecting anything that doesn't match with a structured 422 response — the route handler's own code never even runs for a malformed request. Django's ModelForm sits somewhere between these two: one class handling both storage and form validation together, rather than SQLAlchemy and Pydantic's clean two-layer split.
Database Setup
get_db is written as a generator specifically so FastAPI's own dependency injection can use it — every route that needs a database session declares db: Session = Depends(get_db) and gets one automatically opened and closed around that single request, without writing that setup/teardown logic in every route by hand.
Item instance (the SQLAlchemy model) directly from a route declared to return ItemResponse only works because from_attributes = True tells Pydantic how to read attributes off a non-dict object. Without it, FastAPI would fail trying to serialize the SQLAlchemy object directly — a genuinely common early mistake, conflating "a Python object representing a database row" with "a Python object representing a validated API response," which look similar but are built and behave very differently.
expiryDate || null before sending a request, specifically to avoid an empty string reaching the database where a true NULL was expected. Optional[date] = None expresses the same "not required, genuinely absent" idea directly in the schema's own type — there's no separate normalization step to remember, because the type itself only ever admits a real date or nothing at all.
Where This Course Is Headed
Barcode lookup next — a FastAPI endpoint proxying Open Food Facts, reusing this chapter's own Pydantic/SQLAlchemy split for the lookup cache.
Hands-On Exercises
Explain why ItemCreate has no id, status, or added_at fields, and what happens to a request body that includes a status field anyway.
📄 View solutionExplain the difference between this chapter's validation approach and Food Tracker (React + Express)'s own hand-written validation checks, specifically in terms of whether route handler code ever runs for a malformed request.
📄 View solutionExplain what from_attributes = True actually does, and what would go wrong if a route tried to return a SQLAlchemy Item instance from a route declared to return ItemResponse without it.
📄 View solutionChapter 2 Quick Reference
- Two separate layers: Item (SQLAlchemy, storage) and ItemCreate/ItemResponse (Pydantic, request/response validation)
- ItemCreate: only fields a client may set — no id, status, or added_at, by design, not by convention
- Real advantage: a malformed request is rejected by Pydantic before route handler code ever runs, unlike the Express sibling's own hand-written checks
- from_attributes = True: lets a Pydantic schema read directly from a SQLAlchemy object — required, not automatic
- get_db(): a generator dependency FastAPI uses to open/close a database session per request
- Optional[date] = None: the type itself expresses "genuinely absent," no separate normalization step needed
- Next chapter: Barcode Lookup: Integrating Open Food Facts