FastAPI parses the body, runs Pydantic, and hands your route a typed object. Then the first line of many handlers does this:
@app.post("/users")
async def create_user(body: UserCreate):
data = body.model_dump()
# ... everything else uses data["email"], data.get("name"), ...
You paid for a contract at the edge, then threw the receipt away. From that point on you are back in dict-land: optional keys, nested blobs, and editor silence when a field is renamed.
The model is the API of your handler
A validated model is not ceremony for OpenAPI. It is the shape your code should speak.
Attribute access (body.email) fails loudly when the field is gone. A missing
dict key fails later — or never, if you paper over it with .get.
Keep the instance through the route. Pass it into services. Convert to a dict only at the next real boundary: an ORM insert that wants columns, a message bus, a cache key — places that genuinely need a mapping, not your whole call stack.
Dumping early creates a second, weaker schema
Once you have a dict, every helper invents its own rules. One function expects
"address" as a nested dict; another expects flat keys. One path assumes
age is an int because Pydantic coerced it; another stringifies
it for a query and never converts back.
async def create_user(body: UserCreate, db: Session):
user = User(
name=body.name,
email=body.email,
address_city=body.address.city,
address_zip=body.address.zip,
)
db.add(user)
db.commit()
return UserRead.model_validate(user)
Here the create model stays intact until you map fields on purpose. The response goes through another model so you do not leak internal columns by returning a raw ORM object or an over-broad dump.
When model_dump is the right tool
Use it at serialization boundaries: storing JSON, sending a webhook, building a sparse
update with model_dump(exclude_unset=True) for PATCH. That is dumping with a
reason — not as a reflex because “the rest of the code prefers dicts.”
If most of your app prefers dicts, the fix is not more dumps. It is teaching the app to take models (or smaller typed objects) as parameters.
Validated shape is not the whole story
Keeping the model does not replace authorization, idempotency, or domain rules. It only
preserves the type and structure FastAPI already proved. Do not confuse
UserCreate with “this caller may create this user.” But do not discard the
structure either — you will only rebuild it poorly with dict keys and comments.
Let Pydantic own the edge. Let the model travel inward until something else truly needs bytes or columns.