Debug School

rakesh kumar
rakesh kumar

Posted on

Why FastAPI Uses Pydantic: A Complete Guide to Request Validation with Real-World Examples

Defination
Why use Pydantic
Simple example
Different Pydantic validations
Complete FastAPI example

Defination

Pydantic is used in FastAPI to validate, convert and document incoming and outgoing data.

You define the expected data using a Python class. FastAPI then uses Pydantic automatically to check every request.

Why use Pydantic

Pydantic validation
Powered by type hints — with Pydantic, schema validation and serialization are controlled by type annotations; less to learn, less code to write, and integration with your IDE and static analysis tools. Learn more…
Speed— Pydantic’s core validation logic is written in Rust. As a result, Pydantic is among the fastest data validation libraries for Python. Learn more…
JSON Schema — Pydantic models can emit JSON Schema, allowing for easy integration with other tools. Learn more…
Strict and Lax mode— Pydantic can run in either strict mode (where data is not converted) or lax mode where Pydantic tries to coerce data to the correct type where appropriate. Learn more…

Dataclasses, TypedDicts and more — Pydantic supports validation of many standard library types including dataclass and TypedDict. Learn more…
Customisation — Pydantic allows custom validators and serializers to alter how data is processed in many powerful ways. Learn more…
Ecosystem — around 8,000 packages on PyPI use Pydantic, including massively popular libraries like FastAPI, huggingface, Django Ninja, SQLModel, & LangChain. Learn more…
Battle tested — Pydantic is downloaded over 550M times/month and is used by all FAANG companies and 20 of the 25 largest companies on NASDAQ. If you’re trying to do something with Pydantic, someone else has probably already done it.

Simple example

Suppose your API receives a product order:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Order(BaseModel):
    product_name: str
    quantity: int
    price: float
    is_available: bool = True


@app.post("/orders")
def create_order(order: Order):
    return {
        "message": "Order created successfully",
        "order": order
    }
Enter fullscreen mode Exit fullscreen mode

Valid request:

{
  "product_name": "Laptop",
  "quantity": 2,
  "price": 55000,
  "is_available": true
}
Enter fullscreen mode Exit fullscreen mode

Invalid request:

{
  "product_name": "Laptop",
  "quantity": "two",
  "price": "expensive"
}
Enter fullscreen mode Exit fullscreen mode

FastAPI automatically returns a structured 422 Unprocessable Entity validation response.

Why Pydantic is important in FastAPI
Validates incoming request data.
Converts compatible values into correct Python types.
Produces understandable validation errors.
Generates the OpenAPI and Swagger documentation.
Validates and filters API responses.
Supports nested and complex data structures.
Reduces repetitive manual if conditions.
Improves type hints and editor autocomplete.

Different Pydantic validations

  1. Data-type validation
from pydantic import BaseModel


class User(BaseModel):
    name: str
    age: int
    salary: float
    active: bool
Enter fullscreen mode Exit fullscreen mode

Pydantic verifies that every field contains an appropriate value.

  1. Required and optional fields
from pydantic import BaseModel


class User(BaseModel):
    name: str
    email: str
    phone: str | None = None

Enter fullscreen mode Exit fullscreen mode

name and email are required, while phone is optional.

  1. Default-value validation
from pydantic import BaseModel


class Product(BaseModel):
    name: str
    price: float
    available: bool = True
    currency: str = "INR"
Enter fullscreen mode Exit fullscreen mode

When the client omits available or currency, Pydantic applies the defaults.

  1. Minimum and maximum numbers

Use Field() for numeric restrictions:

from pydantic import BaseModel, Field


class Product(BaseModel):
    name: str
    price: float = Field(gt=0)
    quantity: int = Field(ge=1, le=100)
    discount: float = Field(ge=0, le=50)
Enter fullscreen mode Exit fullscreen mode

Meaning:

Constraint Meaning

gt=0    Greater than 0
ge=1    Greater than or equal to 1
lt=100  Less than 100
le=100  Less than or equal to 100
Enter fullscreen mode Exit fullscreen mode
  1. Minimum and maximum string length
from pydantic import BaseModel, Field


class User(BaseModel):
    username: str = Field(min_length=3, max_length=20)
    password: str = Field(min_length=8, max_length=100)
Enter fullscreen mode Exit fullscreen mode

This prevents usernames or passwords that are too short or long.

  1. Pattern validation
from pydantic import BaseModel, Field


class User(BaseModel):
    username: str = Field(pattern=r"^[a-zA-Z0-9_]+$")
    country_code: str = Field(pattern=r"^[A-Z]{2}$")

Enter fullscreen mode Exit fullscreen mode

Examples:

{
  "username": "ashwani_123",
  "country_code": "IN"
}
Enter fullscreen mode Exit fullscreen mode

Spaces and special characters are rejected from username.

  1. Email validation

Install the email-validation dependency:

pip install "pydantic[email]"
Enter fullscreen mode Exit fullscreen mode

Then use EmailStr:

from pydantic import BaseModel, EmailStr


class User(BaseModel):
    name: str
    email: EmailStr
Enter fullscreen mode Exit fullscreen mode

Valid:

{
  "name": "Ashwani",
  "email": "ashwani@example.com"
}
Enter fullscreen mode Exit fullscreen mode

Invalid:

{
  "name": "Ashwani",
  "email": "ashwani-example"
}
Enter fullscreen mode Exit fullscreen mode
  1. URL validation
from pydantic import BaseModel, HttpUrl


class Website(BaseModel):
    name: str
    url: HttpUrl
Enter fullscreen mode Exit fullscreen mode

Example:

{
  "name": "HolidayLandmark",
  "url": "https://www.holidaylandmark.com"
}
Enter fullscreen mode Exit fullscreen mode
  1. Date and time validation
from datetime import date, datetime
from pydantic import BaseModel


class Trip(BaseModel):
    title: str
    travel_date: date
    booking_time: datetime

Enter fullscreen mode Exit fullscreen mode

Example:

{
  "title": "Manali Adventure",
  "travel_date": "2026-12-15",
  "booking_time": "2026-09-10T10:30:00"
}

Enter fullscreen mode Exit fullscreen mode

Pydantic converts these strings into Python date and datetime objects.

  1. List validation
from pydantic import BaseModel, Field


class Trip(BaseModel):
    title: str
    destinations: list[str] = Field(min_length=1, max_length=10)

Enter fullscreen mode Exit fullscreen mode

At least one destination must be submitted.

{
  "title": "Golden Triangle",
  "destinations": ["Delhi", "Agra", "Jaipur"]
}
Enter fullscreen mode Exit fullscreen mode
  1. Enum validation

Use an enum when only predefined values are allowed:

from enum import Enum
from pydantic import BaseModel


class BookingStatus(str, Enum):
    pending = "pending"
    confirmed = "confirmed"
    cancelled = "cancelled"


class Booking(BaseModel):
    customer_name: str
    status: BookingStatus
Enter fullscreen mode Exit fullscreen mode

The API accepts only pending, confirmed or cancelled.

  1. Nested-object validation
from pydantic import BaseModel, EmailStr


class Customer(BaseModel):
    name: str
    email: EmailStr


class Booking(BaseModel):
    trip_id: int
    travellers: int
    customer: Customer
Enter fullscreen mode Exit fullscreen mode

Request:

{
  "trip_id": 501,
  "travellers": 2,
  "customer": {
    "name": "Ashwani",
    "email": "ashwani@example.com"
  }
}
Enter fullscreen mode Exit fullscreen mode

Pydantic validates both the booking and its nested customer.

  1. Custom field validation

Use field_validator for custom rules:

from pydantic import BaseModel, field_validator


class User(BaseModel):
    username: str
    password: str

    @field_validator("username")
    @classmethod
    def username_must_be_lowercase(cls, value: str) -> str:
        if value != value.lower():
            raise ValueError("Username must contain lowercase letters only")

        return value

    @field_validator("password")
    @classmethod
    def validate_password(cls, value: str) -> str:
        if not any(character.isdigit() for character in value):
            raise ValueError("Password must contain at least one number")

        return value
Enter fullscreen mode Exit fullscreen mode
  1. Cross-field validation

Use model_validator when one field depends on another:

from datetime import date
from pydantic import BaseModel, model_validator


class Trip(BaseModel):
    start_date: date
    end_date: date

    @model_validator(mode="after")
    def validate_dates(self):
        if self.end_date < self.start_date:
            raise ValueError("End date cannot be before start date")

        return self
Enter fullscreen mode Exit fullscreen mode

This validation needs both fields, so model_validator is more appropriate than field_validator.

  1. Decimal and currency validation

For money, prefer Decimal instead of float:

from decimal import Decimal
from pydantic import BaseModel, Field


class Payment(BaseModel):
    amount: Decimal = Field(gt=0, decimal_places=2)
    currency: str = Field(pattern=r"^[A-Z]{3}$")
Enter fullscreen mode Exit fullscreen mode

Example:

{
  "amount": "1499.50",
  "currency": "INR"
}
Enter fullscreen mode Exit fullscreen mode
  1. UUID validation
from uuid import UUID
from pydantic import BaseModel


class BookingRequest(BaseModel):
    booking_id: UUID

Enter fullscreen mode Exit fullscreen mode

Invalid UUID formats are rejected automatically.

  1. Strict validation

By default, Pydantic may convert compatible data. Strict mode prevents unwanted conversion:

from pydantic import BaseModel, ConfigDict


class QuantityRequest(BaseModel):
    model_config = ConfigDict(strict=True)

    quantity: int
Enter fullscreen mode Exit fullscreen mode

With strict mode:

{
  "quantity": "5"
}
Enter fullscreen mode Exit fullscreen mode

is rejected because "5" is a string rather than an integer.

  1. Preventing unexpected fields
from pydantic import BaseModel, ConfigDict


class LoginRequest(BaseModel):
    model_config = ConfigDict(extra="forbid")

    email: str
    password: str
Enter fullscreen mode Exit fullscreen mode

If the client sends an unexpected field, it is rejected:

{
  "email": "user@example.com",
  "password": "secret123",
  "is_admin": true
}

Enter fullscreen mode Exit fullscreen mode

This is especially valuable for security-sensitive APIs.

Complete FastAPI example

from datetime import date
from decimal import Decimal
from enum import Enum

from fastapi import FastAPI
from pydantic import (
    BaseModel,
    ConfigDict,
    EmailStr,
    Field,
    HttpUrl,
    field_validator,
    model_validator,
)

app = FastAPI()


class TripType(str, Enum):
    adventure = "adventure"
    cultural = "cultural"
    leisure = "leisure"


class Customer(BaseModel):
    model_config = ConfigDict(extra="forbid")

    name: str = Field(min_length=2, max_length=80)
    email: EmailStr
    age: int = Field(ge=18, le=100)


class TripBooking(BaseModel):
    model_config = ConfigDict(extra="forbid")

    trip_name: str = Field(min_length=3, max_length=150)
    trip_type: TripType
    start_date: date
    end_date: date
    travellers: int = Field(ge=1, le=20)
    price: Decimal = Field(gt=0, decimal_places=2)
    website: HttpUrl
    customer: Customer
    coupon_code: str | None = Field(
        default=None,
        pattern=r"^[A-Z0-9]{4,12}$"
    )

    @field_validator("trip_name")
    @classmethod
    def clean_trip_name(cls, value: str) -> str:
        return " ".join(value.split())

    @model_validator(mode="after")
    def validate_trip_dates(self):
        if self.end_date < self.start_date:
            raise ValueError("End date cannot be before start date")

        return self


@app.post("/bookings", response_model=TripBooking)
def create_booking(booking: TripBooking):
    return booking

Enter fullscreen mode Exit fullscreen mode

In simple terms, FastAPI manages the API request while Pydantic works like a security guard that checks every piece of data before allowing it into your application.

Top comments (0)