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.
Educational advantages¶
✅ Automatic validation of inputs
✅ Type conversion (fewer parsing errors)
✅ Free documentation (Swagger/OpenAPI)
✅ Model reuse (inputs and outputs)
Conclusion¶
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.