FastAPI Request Validation with Pydantic
Pydantic request models, automatic validation errors, field constraints, and response models.
Pydantic models for request bodies
Path and query parameters cover simple values, but a real API endpoint (creating a user, placing an order) usually needs a structured JSON body. FastAPI uses Pydantic models for this — a Pydantic model is a plain Python class with type-annotated fields:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class UserCreate(BaseModel):
name: str
email: str
age: int | None = None # optional, defaults to None if omitted
@app.post("/users")
def create_user(user: UserCreate):
return {"message": f"Created {user.name}", "email": user.email}
Declaring the parameter's type as UserCreate (a BaseModel subclass) tells FastAPI to read the JSON request body, validate every field against the model, and hand you back a real, fully-typed Python object — user.name, user.email, and user.age are ordinary attributes, not raw dict keys you have to remember to spell correctly.
Automatic validation and clear error responses
Send a request that violates the model — a missing required field, or a value of the wrong type:
curl -X POST http://127.0.0.1:8000/users \
-H "Content-Type: application/json" \
-d '{"name": "Ada"}'
FastAPI responds with 422 Unprocessable Entity and a body like:
{
"detail": [
{
"type": "missing",
"loc": ["body", "email"],
"msg": "Field required",
"input": {"name": "Ada"}
}
]
}
create_user() never runs at all — invalid input is rejected before your business logic sees it, and the error message tells the client exactly which field is wrong and why, with no manual if checks written on your end.
Adding richer field constraints
Field() attaches validation rules and documentation metadata to individual fields:
from pydantic import BaseModel, EmailStr, Field
class UserCreate(BaseModel):
name: str = Field(min_length=1, max_length=80)
email: EmailStr # validates real email address format
age: int | None = Field(default=None, ge=0, le=150)
EmailStr (from pydantic[email], an optional extra) validates that a string is a plausible email address, not just any string. ge/le (greater-than-or-equal / less-than-or-equal) constrain a number's range — an age of -5 or 200 is rejected automatically.
Response models
Just as an input model shapes what a client can send, a response model shapes what your API sends back — useful for hiding fields the client shouldn't see (like a hashed password) even if your internal object has them:
class UserCreate(BaseModel):
name: str
email: EmailStr
password: str
class UserOut(BaseModel):
id: int
name: str
email: EmailStr
# note: no `password` field here at all
@app.post("/users", response_model=UserOut)
def create_user(user: UserCreate):
saved = save_to_database(user) # your own persistence logic, returns something with id/name/email/password
return saved
Even though whatever save_to_database() returns might still carry a password attribute internally, response_model=UserOut tells FastAPI to filter and serialize only the fields defined on UserOut — the password never reaches the JSON response. This also means the /docs page accurately documents the real response shape, separately from the request shape.
Common mistakes
- Reusing one model for both request input and response output — it's convenient at first, but it means any sensitive or internal-only field (a password, an internal flag) has to be manually stripped out by hand instead of simply not existing on the response model.
- Assuming Pydantic validation replaces business-rule validation — Pydantic checks shape and type ("is this a valid email-looking string, is this an int"), not business rules like "is this email already registered," which still belongs in your own endpoint logic.
- Forgetting that a field with no default is required — leaving it out of the request body is a validation error, not an implicit
None, unless you explicitly wrote| None = None.
Interview questions
Q: Why does FastAPI use type hints to drive request validation, instead of a separate validation schema or library?
Type hints are something Python developers already write for editor autocomplete and static type checkers, so FastAPI (via Pydantic) reuses that exact same information to validate incoming data, generate the OpenAPI schema, and populate the /docs page — one source of truth instead of three things (the code, a validation schema, and API documentation) that could drift out of sync with each other.
Q: What's the purpose of a response_model, given that FastAPI can already serialize whatever you return?
It explicitly constrains and documents the output shape independently of whatever object your endpoint returns internally. This matters most for security (stripping a password hash or internal ID that the underlying object carries but a client should never see) and for accurate API documentation, since the request shape and response shape are very often not identical.