FastAPI relies on Pydantic to handle data validation and serialization.
This is one of the framework’s strengths:
you describe your data with annotated Python classes, and FastAPI takes care of the rest.
Define a data model¶
A Pydantic model is a class that inherits from BaseModel.
Each attribute is typed with standard Python annotations:
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str
email: str
is_active: bool = True # default value➡️ Here, id and name are required, is_active is optional because it has a default value.
Use a model as request body¶
In FastAPI, when you declare a parameter of type BaseModel, FastAPI will automatically read the JSON from the request body and verify that the data matches.
from fastapi import FastAPI
app = FastAPI()
@app.post("/users/")
def create_user(user: User):
# of course you would save the user in a database here..
# we'll come back to that in the next episode !
return {"message": f"User {user.name} created", "data": user}Call example¶
http POST :8000/users/ id:=1 name="Alice" email="alice@example.com"✅ FastAPI transforms the incoming (body) JSON into a User object
✅ it converts types if needed (e.g. id to int)
✅ if a value is missing or of the wrong type, a 422 error is returned automatically.
Automatic validation and transformation¶
Pydantic doesn’t just check types: it also converts data when possible.
class Product(BaseModel):
name: str
price: float
in_stock: boolhttp -v POST :8000/products/ name="Pen" price="9.99" in_stock=true➡️ price="9.99" is a string, but Pydantic converts it to float by FastAPI
➡️ in_stock=true, same, and is converted to bool
reminder, = vs := with httpie
= vs := with httpieremember that in httpie:= sends a string, while:= sends a typed value (int, float, bool, etc.)
try them both and look at the differences!
Advanced validation - constraints¶
Pydantic offers simple constraints:
from pydantic import BaseModel, Field
class Signup(BaseModel):
username: str = Field(..., min_length=3, max_length=20)
password: str = Field(..., min_length=8)
age: int = Field(..., ge=18) # ge = greater or equal...means “required”.Constraints are automatically documented in the OpenAPI doc.
Error example¶
http POST :8000/signup username="ab" password="123" age:=15Response (automatically generated by FastAPI too):
{
"detail": [
{"loc": ["body", "username"], "msg": "String should have at least 3 characters"},
{"loc": ["body", "password"], "msg": "String should have at least 8 characters"},
{"loc": ["body", "age"], "msg": "Input should be greater than or equal to 18"}
]
}Nested models¶
A model can contain other models:
class Address(BaseModel):
city: str
zipcode: str
class Customer(BaseModel):
name: str
address: AddressFastAPI handles deserialization automatically:
{
"name": "Bob",
"address": {
"city": "Paris",
"zipcode": "75001"
}
}Responses with Pydantic¶
You can also declare an endpoint’s output schema with response_model:
@app.get("/users/{user_id}", response_model=User)
def get_user(user_id: int):
return {"id": user_id, "name": "Alice", "email": "alice@example.com", "is_active": True}➡️ FastAPI will return a validated response according to User.
➡️ Additional fields (the ones not defined in the model) are automatically excluded.
Useful to avoid exposing sensitive data, like e.g. passwords.
The cool features of Pydantic¶
With Pydantic, you describe your data once in the form of Python classes.
FastAPI takes care of:
reading the JSON from the client,
validating and converting fields,
producing clear documentation,
ensuring your responses respect the contract.
👉 It’s good practice to systematically use Pydantic models for your endpoints that consume or produce structured data.