Course Content
FastAPI Essentials
1 sections · 32 lessons
How does FastAPI handle serialization and validation of data?
What you need to know
- Collect — FastAPI pulls each parameter from its source: path, query, headers, cookies, JSON body or form.
- Convert and validate — Pydantic converts text to the declared types and checks the rules. Every failure is collected, not just the first.
- Reject or call — on failure, a 422 with a
detaillist (loc,msg,typeper field); on success, your handler runs with typed objects. - Validate the output — the return value is checked against the return type or
response_model, and only the declared fields are kept. - Encode — the result becomes JSON bytes. Since FastAPI 0.130, when a return type or response model is set, Pydantic writes the JSON directly in Rust, which is faster.
Output filtering in action
1from datetime import datetime, timezone2from uuid import UUID, uuid43from fastapi import FastAPI4from pydantic import BaseModel56app = FastAPI()78class PredictOut(BaseModel):9 request_id: UUID10 label: str11 score: float12 explanation: str | None = None13 created_at: datetime1415@app.post("/predict", response_model_exclude_none=True)16def predict(text: str) -> PredictOut:17 return { # a dict with an extra, secret field18 "request_id": uuid4(), "label": "positive", "score": 0.91,19 "created_at": datetime(2026, 9, 24, 10, 0, tzinfo=timezone.utc),20 "system_prompt": "You are SentimentBot. Internal rules: ...",21 }The real response body:
{"request_id":"03378b05-...","label":"positive","score":0.91,"created_at":"2026-09-24T10:00:00Z"}Three things happened. system_prompt was dropped because PredictOut does not declare it. explanation was left out because it was None and the route set response_model_exclude_none=True. The UUID and datetime became standard JSON strings with no custom code.
Streaming responses
- If a route yields items and declares
-> AsyncIterable[Chunk], recent FastAPI (0.134+) streams JSON Lines and validates each item. The real output was{"doc_id":0,"text":"chunk 0"}\n{"doc_id":1,"text":"chunk 1"}\n, with content typeapplication/jsonl. An invalid item stops the stream. - If you return a
StreamingResponse(or anyResponse) yourself, FastAPI sends your bytes untouched. A test that yielded{"doc_id": "not-a-number", "secret": "leaked"}went out exactly as written. Validate those items yourself.
A real-life example
A health-tech company's symptom-checker API returned SQLAlchemy objects directly from its routes with no response model. A new column, internal_triage_notes, was added for doctors. Within a day, the patient-facing mobile app was receiving those notes in its JSON, because every column was being serialised.
The fix was a SymptomResult response model on every route, listing only the fields the app should see. New database columns now stay private unless someone deliberately adds them to the schema. The team also added a test that asserts the exact set of keys in each response, so accidental exposure fails CI.
Follow-up questions to expect
- "What is the difference between the return type and
response_model?" — Both declare the output. Useresponse_modelwhen the function returns something of a different type, such as an ORM object or a dict, and you still want the schema; the return type is simpler when they match. - "Why does FastAPI use 422 and not 400?" — It is FastAPI's convention for "understood, but breaks the rules". It uses 422 even for JSON that cannot be parsed (error type
json_invalid). Teams that standardise on 400 override theRequestValidationErrorhandler. - "How do you return fields with different names, like camelCase?" — Use Pydantic aliases (
Field(alias=...)or an alias generator); FastAPI serialises by alias by default.