KAHIBARO
Discord Login Register

Pydantic Models

Why Pydantic Matters in FastAPI

FastAPI is built around data validation and data serialization. Pydantic is the library that powers this. Every time you:

FastAPI uses Pydantic models to convert raw data into Python objects and validate that the data is correct.

Pydantic models are like "smart dictionaries" with:

In this chapter, you will see how to use Pydantic models in FastAPI for both input and output.


Basic Pydantic Model

A Pydantic model is a Python class that inherits from BaseModel and uses type hints.

python
from pydantic import BaseModel
class User(BaseModel):
    id: int
    name: str
    email: str
    is_active: bool

This defines a data shape:

FieldTypeDescription
idintNumeric identifier
namestrUser's name
emailstrUser's email
is_activeboolWhether the user is active

Pydantic will:

Key rule: Every field in a Pydantic model must have a type hint. Pydantic uses type hints to validate and convert data.


Using Models in FastAPI Requests

Pydantic models are most visible in request bodies. You declare them as function parameters.

python
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    in_stock: bool = True
@app.post("/items/")
async def create_item(item: Item):
    # item is a validated Pydantic model here
    return {"message": "Item received", "item": item}

What happens when a client sends JSON to /items/:

  1. FastAPI reads the body and parses JSON.
  2. It uses the Item model to validate the data.
  3. It creates an Item instance and passes it as the item parameter.
  4. If validation fails, FastAPI returns a 422 error with details.

Example request

Request body:

json
{
  "name": "Laptop",
  "description": "A powerful laptop",
  "price": 1299.99,
  "in_stock": false
}

Inside the endpoint, item is an Item instance:

python
item.name        # "Laptop"
item.price       # 1299.99
item.in_stock    # False
item.description # "A powerful laptop"

If the client sends:

json
{
  "name": "Laptop",
  "price": "1299.99"
}

Pydantic will:

Default Values and Optional Fields

You can control which fields are required and which are optional.

Required fields

Fields without a default value are required.

python
class Product(BaseModel):
    name: str          # required
    price: float       # required
    in_stock: bool     # required

All three must be provided.

Optional fields with default values

Fields with a default value are optional.

python
class Product(BaseModel):
    name: str
    price: float
    in_stock: bool = True  # optional, defaults to True

Here, the client can omit in_stock. It becomes True.

Optional type vs optional value

python
from typing import Optional
class UserProfile(BaseModel):
    username: str
    bio: Optional[str] = None
    age: int | None = None

If you want a field that must be present but can be null in JSON:

python
from pydantic import BaseModel
class Example(BaseModel):
    value: int | None  # required, but can be null

The client must send "value": null or a number. Omitting value causes a validation error.


Field Types and Validation

Pydantic supports many types and performs validation based on them.

Some common types:

TypeExampleValidation example
intage: intRejects non integer values
floatprice: floatConverts "10.5" to 10.5
boolis_active: boolConverts "true" to True
strname: strConverts numbers to strings if possible
datetimecreated_at: datetimeParses ISO datetime strings
datebirthday: dateParses "2020-01-01"
list[int]tags: list[int]Ensures a list, with each element an int
dict[str, int]scores: dict[str, int]Keys are strings, values are ints
EmailStremail: EmailStrValidates email format
UUIDid: UUIDValidates UUID string
HttpUrlurl: HttpUrlValidates URL format

You can import some special types from pydantic:

python
from pydantic import BaseModel, EmailStr, HttpUrl
class Contact(BaseModel):
    email: EmailStr
    website: HttpUrl | None = None

If the client sends an invalid email, Pydantic returns an error.


Nested Models

You can nest Pydantic models inside each other. This is very common in real APIs.

python
from pydantic import BaseModel
class Address(BaseModel):
    street: str
    city: str
    country: str
class User(BaseModel):
    id: int
    name: str
    address: Address

Example JSON for a User:

json
{
  "id": 1,
  "name": "Alice",
  "address": {
    "street": "Main Street 1",
    "city": "Springfield",
    "country": "USA"
  }
}

Pydantic will:

In your code:

python
user.address.city  # "Springfield"

You can also have lists of nested models:

python
class OrderItem(BaseModel):
    product_id: int
    quantity: int
class Order(BaseModel):
    id: int
    items: list[OrderItem]

Request body example:

json
{
  "id": 123,
  "items": [
    {"product_id": 1, "quantity": 2},
    {"product_id": 2, "quantity": 1}
  ]
}

Models for Input vs Output

You do not need to use the same model for input and output. Often, you should not.

Examples:

python
from pydantic import BaseModel
class UserCreate(BaseModel):
    name: str
    email: str
    password: str
class UserRead(BaseModel):
    id: int
    name: str
    email: str
    is_active: bool

You can use them in FastAPI:

python
from fastapi import FastAPI
app = FastAPI()
@app.post("/users/", response_model=UserRead)
async def create_user(user_in: UserCreate):
    # Example: pretend this user was saved to a database
    db_user = {
        "id": 1,
        "name": user_in.name,
        "email": user_in.email,
        "is_active": True,
        "hashed_password": "not-shown"
    }
    # FastAPI will convert this dict into a UserRead
    return db_user

Important points:

Important rule: Use separate models for input and output when you need to hide sensitive data, such as passwords or internal fields.


Model Configuration and Aliases

Sometimes JSON field names do not match Python variable names. For example, the client sends user_id but you prefer userId in Python, or you must follow an existing external API.

You can define field aliases using Field.

python
from pydantic import BaseModel, Field
class Item(BaseModel):
    item_id: int = Field(alias="itemId")
    price_in_cents: int = Field(alias="priceInCents")

Now Pydantic will:

json
  {
    "itemId": 10,
    "priceInCents": 999
  }

By default, when converting the model back to JSON, Pydantic uses the internal field names, unless you specify by_alias=True:

python
item = Item(itemId=10, priceInCents=999)
item.dict()          # {'item_id': 10, 'price_in_cents': 999}
item.dict(by_alias=True)  # {'itemId': 10, 'priceInCents': 999}

In FastAPI, response_model uses by_alias=True by default, so clients see aliases.

You can control validation behavior with Config (Pydantic v1) or model_config (Pydantic v2). Since FastAPI is moving to Pydantic v2, here is v2 style:

python
from pydantic import BaseModel, ConfigDict
class Item(BaseModel):
    model_config = ConfigDict(
        populate_by_name=True,  # allow using internal field names too
        extra="forbid"          # reject unknown fields
    )
    item_id: int = Field(alias="itemId")

Now:

Converting Models to Dicts and JSON

Pydantic models are normal Python objects, but they have helpful methods.

python
from pydantic import BaseModel
class Item(BaseModel):
    name: str
    price: float
    description: str | None = None
item = Item(name="Book", price=9.99)

You can convert to a dictionary:

python
data = item.model_dump()
# {'name': 'Book', 'price': 9.99, 'description': None}

Common options:

python
item.model_dump(exclude_none=True)
# {'name': 'Book', 'price': 9.99}
item.model_dump(include={"name", "price"})
# {'name': 'Book', 'price': 9.99}
item.model_dump(exclude={"description"})
# {'name': 'Book', 'price': 9.99}

You can also get a JSON string:

python
json_str = item.model_dump_json()
# '{"name": "Book", "price": 9.99, "description": null}'

In FastAPI endpoints, you rarely need to call these manually, because FastAPI converts return values into JSON automatically, using the response_model when defined.


Validation Errors and Error Responses

When data does not match the Pydantic model, FastAPI returns a 422 response with error details.

Example model:

python
class Item(BaseModel):
    name: str
    price: float

Client sends:

json
{
  "name": 123,
  "price": "abc"
}

Pydantic will:

The client gets a response like:

json
{
  "detail": [
    {
      "type": "float_parsing",
      "loc": ["body", "price"],
      "msg": "Input should be a valid number",
      "input": "abc"
    }
  ]
}

This strongly typed validation is one of the main reasons FastAPI and Pydantic work so well together.


Example: Simple CRUD Shapes

Imagine you are building a basic API for managing tasks. You might define several Pydantic models.

python
from pydantic import BaseModel
from datetime import datetime
class TaskBase(BaseModel):
    title: str
    description: str | None = None
class TaskCreate(TaskBase):
    # same fields as TaskBase, no extra fields
    pass
class TaskUpdate(BaseModel):
    title: str | None = None
    description: str | None = None
    completed: bool | None = None
class TaskRead(TaskBase):
    id: int
    completed: bool
    created_at: datetime

Use them in FastAPI:

python
from fastapi import FastAPI
app = FastAPI()
@app.post("/tasks/", response_model=TaskRead)
async def create_task(task_in: TaskCreate):
    # fake database object
    db_task = {
        "id": 1,
        "title": task_in.title,
        "description": task_in.description,
        "completed": False,
        "created_at": datetime.utcnow(),
    }
    return db_task
@app.patch("/tasks/{task_id}", response_model=TaskRead)
async def update_task(task_id: int, task_in: TaskUpdate):
    # use only the fields that are not None
    update_data = task_in.model_dump(exclude_unset=True)
    # pretend we update the database with update_data
    # and then return the updated task
    updated_task = {
        "id": task_id,
        "title": update_data.get("title", "Old title"),
        "description": update_data.get("description", "Old desc"),
        "completed": update_data.get("completed", False),
        "created_at": datetime.utcnow(),
    }
    return updated_task

Important here:

Summary

In the next chapters on FastAPI, these Pydantic models will become the foundation for request bodies, response models, and advanced validation.

Views: 7

Comments

Please login to add a comment.

Don't have an account? Register now!