Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

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: bool
http -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


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

Error example

http POST :8000/signup username="ab" password="123" age:=15

Response (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: Address

FastAPI 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


Conclusion

With Pydantic, you describe your data once in the form of Python classes.
FastAPI takes care of:

👉 It’s good practice to systematically use Pydantic models for your endpoints that consume or produce structured data.