In the previous article, we connected our FastAPI application to a database using SQLite and SQLAlchemy.
We also used classes like:
class StudentCreate(BaseModel):
name: str
department: str
cgpa: float
without fully understanding what was happening behind the scenes.
Today, we'll fix that.
If you haven't read it check it out:
Why Do We Need Data Validation?
Imagine you're building a weather application.
A user asks:
What is the temperature in Chennai?
A valid response might be:
35
or
35°C
But what if the API returns:
Sunny
This is clearly wrong.
Temperature should be represented as a number.
Even if the value itself is inaccurate, we still know that temperature must be numeric.
This is where validation becomes important.
Validation allows us to define rules about what data is acceptable before it enters our application.
For example:
- Temperature should be numeric
- Age cannot be negative
- CGPA should be between 0 and 10
- Email addresses should follow a valid format
Without validation, applications can receive invalid data and behave unexpectedly.
The Problem Without Validation
Consider a student registration API.
@app.post("/student")
def create_student(student):
return student
A user could send:
{
"name": "Ananya",
"cgpa": "Excellent"
}
The API would accept it.
But a CGPA should be a number, not text.
As applications grow, manually checking every field becomes difficult.
We need a better solution.
Enter Pydantic
Pydantic is a Python library used for data validation.
FastAPI uses Pydantic extensively behind the scenes.
Instead of manually validating data, we define a schema.
from pydantic import BaseModel
class Student(BaseModel):
name: str
cgpa: float
Now FastAPI knows:
-
namemust be a string -
cgpamust be a floating-point number
Whenever data arrives, FastAPI automatically validates it.
Your First Pydantic Model
from pydantic import BaseModel
class Student(BaseModel):
name: str
department: str
cgpa: float
Think of this model as a blueprint.
Any incoming request must follow this structure.
Valid Request
{
"name": "Ananya",
"department": "CSE",
"cgpa": 8.9
}
Invalid Request
{
"name": "Ananya",
"department": "CSE",
"cgpa": "Excellent"
}
FastAPI will reject the request automatically.
Using Pydantic with FastAPI
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Student(BaseModel):
name: str
department: str
cgpa: float
@app.post("/student")
def create_student(student: Student):
return student
Notice this line:
student: Student
FastAPI now expects the incoming request body to match the Student schema.
Understanding Validation Errors
Suppose we send:
{
"name": "Ananya",
"department": "CSE",
"cgpa": "Excellent"
}
FastAPI returns a validation error before the request reaches our route.
You'll see an error similar to:
{
"detail": [
{
"type": "float_parsing",
"msg": "Input should be a valid number"
}
]
}
Instead of failing silently, FastAPI clearly tells us what went wrong.
Adding Constraints with Field()
Validating types is useful.
But sometimes we need stricter rules.
For example:
- CGPA should be between 0 and 10
- Name should have a minimum length
- Age should always be positive
Pydantic provides Field() for this purpose.
from pydantic import BaseModel, Field
class Student(BaseModel):
name: str = Field(
min_length=2,
max_length=50
)
cgpa: float = Field(
gt=0,
lt=10
)
Understanding the Constraints
cgpa: float = Field(gt=0, lt=10)
This means:
CGPA > 0
CGPA < 10
Valid Request
{
"name": "Ananya",
"cgpa": 8.9
}
Invalid Request
{
"name": "Ananya",
"cgpa": 15
}
FastAPI immediately rejects the request.
Optional Fields
Sometimes fields are not mandatory.
For example, a student may not have a department assigned yet.
from typing import Optional
from pydantic import BaseModel
class Student(BaseModel):
name: str
department: Optional[str] = None
Now the department field becomes optional.
Valid Request
{
"name": "Ananya"
}
Also Valid
{
"name": "Ananya",
"department": "CSE"
}
Request Models vs Database Models
One question many beginners ask is:
Why do we need both
schemas.pyandmodels.py?
Let's understand the difference.
SQLAlchemy Model
class Student(Base):
__tablename__ = "students"
id = Column(Integer, primary_key=True)
name = Column(String)
cgpa = Column(Float)
This defines how data is stored in the database.
Pydantic Model
class StudentCreate(BaseModel):
name: str
cgpa: float
This defines how data enters our API.
Think of it this way:
Database Structure
≠
API Structure
They may look similar, but they serve different purposes.
Response Models
Pydantic can also control what data is returned from an API.
class StudentResponse(BaseModel):
id: int
name: str
cgpa: float
@app.get(
"/student/{id}",
response_model=StudentResponse
)
def get_student(id: int):
...
This ensures the response always follows a consistent structure.
Why Pydantic Matters for AI Applications
Suppose you're building an LLM API.
Expected request:
{
"prompt": "Explain FastAPI",
"temperature": 0.7,
"max_tokens": 500
}
Without validation, a user could send:
{
"prompt": "Explain FastAPI",
"temperature": "very creative",
"max_tokens": "a lot"
}
and your application would have to deal with invalid data.
Pydantic prevents invalid requests from ever reaching your business logic.
This becomes especially important when building:
- AI chatbots
- RAG applications
- Agentic systems
- Model inference APIs
- Multi-agent workflows
How Validation Fits into the Request Lifecycle
Client Request
│
▼
Pydantic Validation
│
▼
FastAPI Route
│
▼
Business Logic
│
▼
Database / LLM
Pydantic acts as the first line of defense.
Only valid data reaches the rest of the application.
Conclusion
Pydantic is one of the reasons FastAPI has become so popular. It allows us to build APIs that are safer, more predictable, and easier to maintain.
In the next article, we'll move into Authentication and Authorization and learn how to protect our APIs from unauthorized access.
Top comments (0)