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:

Python
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:

Bash
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:

JSON
{
  "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:

Python
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:

Python
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.