DEV Community

Cover image for FastAPI for AI Engineers - Part 4: Stop Bad Data Before It Breaks Your API (Pydantic and Data Validation)
Ananya S
Ananya S

Posted on

FastAPI for AI Engineers - Part 4: Stop Bad Data Before It Breaks Your API (Pydantic and Data Validation)

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

or

35°C
Enter fullscreen mode Exit fullscreen mode

But what if the API returns:

Sunny
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

A user could send:

{
    "name": "Ananya",
    "cgpa": "Excellent"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now FastAPI knows:

  • name must be a string
  • cgpa must 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
Enter fullscreen mode Exit fullscreen mode

Think of this model as a blueprint.

Any incoming request must follow this structure.

Valid Request

{
    "name": "Ananya",
    "department": "CSE",
    "cgpa": 8.9
}
Enter fullscreen mode Exit fullscreen mode

Invalid Request

{
    "name": "Ananya",
    "department": "CSE",
    "cgpa": "Excellent"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Notice this line:

student: Student
Enter fullscreen mode Exit fullscreen mode

FastAPI now expects the incoming request body to match the Student schema.


Understanding Validation Errors

Suppose we send:

{
    "name": "Ananya",
    "department": "CSE",
    "cgpa": "Excellent"
}
Enter fullscreen mode Exit fullscreen mode

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"
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

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
    )
Enter fullscreen mode Exit fullscreen mode

Understanding the Constraints

cgpa: float = Field(gt=0, lt=10)
Enter fullscreen mode Exit fullscreen mode

This means:

CGPA > 0
CGPA < 10
Enter fullscreen mode Exit fullscreen mode

Valid Request

{
    "name": "Ananya",
    "cgpa": 8.9
}
Enter fullscreen mode Exit fullscreen mode

Invalid Request

{
    "name": "Ananya",
    "cgpa": 15
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now the department field becomes optional.

Valid Request

{
    "name": "Ananya"
}
Enter fullscreen mode Exit fullscreen mode

Also Valid

{
    "name": "Ananya",
    "department": "CSE"
}
Enter fullscreen mode Exit fullscreen mode

Request Models vs Database Models

One question many beginners ask is:

Why do we need both schemas.py and models.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)
Enter fullscreen mode Exit fullscreen mode

This defines how data is stored in the database.


Pydantic Model

class StudentCreate(BaseModel):
    name: str
    cgpa: float
Enter fullscreen mode Exit fullscreen mode

This defines how data enters our API.


Think of it this way:

Database Structure
        ≠
API Structure
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
@app.get(
    "/student/{id}",
    response_model=StudentResponse
)
def get_student(id: int):
    ...
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Without validation, a user could send:

{
    "prompt": "Explain FastAPI",
    "temperature": "very creative",
    "max_tokens": "a lot"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)