Master FastAPI Request Body Validation with Pydantic Models
This guide explains how FastAPI uses Pydantic models to define, validate, and parse JSON and form request bodies, set default values and custom rules, handle nested structures, and return response models, providing a type‑safe, efficient way to build backend APIs.
When a client sends a POST, PUT, or PATCH request, the data in the request body can be defined using Pydantic models in FastAPI.
Processing JSON request bodies
FastAPI automatically converts incoming JSON payloads into Python objects based on the Pydantic model.
<code>from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
# Define data model
class User(BaseModel):
username: str
email: str
age: int
@app.post("/users/")
def create_user(user: User):
return {"message": "User created successfully", "user": user}
</code>The User model specifies the expected fields (username, email, age). The create_user endpoint validates the JSON and returns the parsed User object.
Example JSON request
<code>{
"username": "john_doe",
"email": "[email protected]",
"age": 25
}
</code>Example response:
<code>{
"message": "User created successfully",
"user": {
"username": "john_doe",
"email": "[email protected]",
"age": 25
}
}
</code>Pydantic internally uses BaseModel.parse_obj() to:
Check field matching (missing or extra fields)
Perform type conversion (e.g., string to int)
Apply validation rules (regex, range, etc.)
Instantiate the model (User object)
It leverages Python type hints to automatically parse int, float, str, bool, datetime, UUID, Decimal, and recursively handle nested models or lists.
Setting default values and validation rules
Pydantic offers rich field validation, allowing defaults, constraints, and regex patterns.
<code>from pydantic import BaseModel, Field, EmailStr
class User(BaseModel):
username: str = Field(..., min_length=3, max_length=20)
email: EmailStr
age: int = Field(..., gt=0, lt=120) # age must be >0 and <120
</code>Field(..., min_length=3) makes the field required with a minimum length of 3.
EmailStr validates email format automatically.
gt and lt set numeric ranges.
Custom validators can be added:
<code>from pydantic import validator
class User(BaseModel):
username: str
age: int
@validator('username')
def name_must_not_be_empty(cls, v):
if not v.strip():
raise ValueError('username cannot be empty')
return v
</code>If validation fails, FastAPI returns a 422 response with detailed error information:
<code>{
"detail": [
{
"loc": ["body", "age"],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
]
}
</code>Validation logic can be reused with dependency injection:
<code>from fastapi import Depends, HTTPException
def verify_age(user: User):
if user.age < 18:
raise HTTPException(status_code=400, detail="User must be 18 or older")
return user
@app.post("/register/")
def register_user(user: User = Depends(verify_age)):
return {"message": "User registered", "user": user}
</code>Handling form data with Form()
To receive form fields instead of JSON, install python-multipart and use Form :
<code>pip install python-multipart
</code> <code>from fastapi import Form, FastAPI
from typing import Annotated
app = FastAPI()
@app.post("/login/")
def login(
username: Annotated[str, Form()],
password: Annotated[str, Form()]
):
return {"username": username, "password": password}
</code>Example curl request:
<code>curl -X POST "http://127.0.0.1:8000/login" -d "username=john_doe&password=secret"
</code>Response:
<code>{
"username": "john_doe",
"password": "secret"
}
</code>Nested models
Complex structures can be built by nesting Pydantic models:
<code>class Address(BaseModel):
city: str
zip_code: str
class User(BaseModel):
username: str
email: str
age: int
address: Address
</code> <code>{
"username": "john_doe",
"email": "[email protected]",
"age": 25,
"address": {
"city": "New York",
"zip_code": "10001"
}
}
</code>Response models
Response models hide internal fields and enforce a consistent output format:
<code>class UserResponse(BaseModel):
username: str
email: str
@app.post("/users/", response_model=UserResponse)
def create_user(user: User):
return user
</code>If the User model contains a password field, it will not be returned because the response model only includes the defined fields.
Summary
FastAPI leverages Pydantic’s powerful validation to simplify request‑body handling for JSON, form data, and file uploads, enabling developers to build reliable, type‑safe APIs quickly.
Further reading:
FastAPI official documentation: https://fastapi.tiangolo.com/
Pydantic documentation: https://docs.pydantic.dev/
Code Mala Tang
Read source code together, write articles together, and enjoy spicy hot pot together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.