Create Pydantic v2 base schemas for request validation and response serialization in FastAPI
FastAPI Core Schemas
Overview
This skill covers creating base Pydantic v2 schemas for consistent request/response handling across the application.
Create core/schemas.py
Create src/app/core/schemas.py:
from datetime import datetime
from uuid import UUID
from pydantic import BaseModel, ConfigDict
classBaseSchema(BaseModel):
"""
Base schema for all Pydantic models.
Configured with:
- from_attributes: Enables ORM mode (read from SQLAlchemy models)
- populate_by_name: Allow using field names or aliases
- str_strip_whitespace: Strip whitespace from string fields
- validate_default: Validate default values
"""
model_config = ConfigDict(
from_attributes=True,
populate_by_name=True,
str_strip_whitespace=True,
validate_default=True,
)
classBaseCreateSchema(BaseSchema):
"""
Base schema for create operations.
Does NOT include id, timestamps, or deleted_at.
Only fields that the client provides when creating a resource.
"""passclass ():
():
: UUID
created_at: datetime
updated_at: datetime
():
deleted_at: datetime | =
BaseUpdateSchema
BaseSchema
"""
Base schema for update operations.
All fields should be Optional to support partial updates (PATCH).
Does NOT include id, timestamps, or deleted_at.
"""
pass
class
BaseResponseSchema
BaseSchema
"""
Base schema for response serialization.
Includes:
- id: UUID primary key
- created_at: Creation timestamp
- updated_at: Last update timestamp
"""
id
class
BaseResponseWithDeletedSchema
BaseResponseSchema
"""
Response schema that includes soft delete information.
Use when the API needs to return deleted_at field,
such as admin endpoints or trash/archive views.
"""
None
None
Usage Example
When creating entity schemas, inherit from the base schemas:
# src/app/items/schemas.pyfrom uuid import UUID
from pydantic import Field
from app.core.schemas import (
BaseCreateSchema,
BaseUpdateSchema,
BaseResponseSchema,
)
classItemCreate(BaseCreateSchema):
"""Schema for creating an item."""
name: str = Field(..., min_length=1, max_length=255)
description: str | None = Field(default=None, max_length=5000)
category_id: UUID
classItemUpdate(BaseUpdateSchema):
"""Schema for updating an item. All fields optional for PATCH."""
name: str | None = Field(default=None, min_length=1, max_length=255)
description: str | None = Field(default=None, max_length=5000)
category_id: UUID | None = NoneclassItemResponse(BaseResponseSchema):
"""Schema for item responses."""
name: str
description: str | None
category_id: UUID
Pydantic v2 Configuration Options
Option
Purpose
Default
from_attributes
Read data from ORM model attributes
False
populate_by_name
Allow field name or alias
False
str_strip_whitespace
Strip whitespace from strings
False
validate_default
Validate default values
False
strict
Strict type coercion
False
extra
Handle extra fields: "ignore", "forbid", "allow"
"ignore"
Field Validation
from pydantic import Field, field_validator, model_validator
classItemCreate(BaseCreateSchema):
name: str = Field(
..., # Required
min_length=1,
max_length=255,
description="Item name",
examples=["My Item"],
)
price: float = Field(
...,
gt=0, # Greater than 0
le=1000000, # Less than or equal to
description="Item price in USD",
)
tags: list[str] = Field(
default_factory=list,
max_length=10, # Max 10 tags
)
@field_validator("name") @classmethoddefvalidate_name(cls, v: str) -> str:
"""Custom name validation."""if v.lower() == "test":
raise ValueError("Name cannot be 'test'")
return v.title()
@model_validator(mode="after")defvalidate_model(self) -> "ItemCreate":
"""Cross-field validation."""ifself.price > 100andnotself.tags:
raise ValueError("Expensive items must have tags")
returnself
from typing importGeneric, TypeVar
from pydantic import BaseModel
T = TypeVar("T")
classListResponse(BaseModel, Generic[T]):
"""Generic list response with metadata."""
items: list[T]
total: int
model_config = ConfigDict(from_attributes=True)
# Usage:# ListResponse[ItemResponse]
Integration with fastapi-pagination
When using fastapi-pagination, you don't need custom list schemas. The library provides Page[T]:
from pydantic import computed_field
classItemResponse(BaseResponseSchema):
name: str
price: float
quantity: int @computed_field @propertydeftotal_value(self) -> float:
"""Computed field for total value."""returnself.price * self.quantity
Serialization Aliases
from pydantic import Field
classItemResponse(BaseResponseSchema):
internal_id: str = Field(serialization_alias="id")
item_name: str = Field(serialization_alias="name")