Back to blog

Làm Chủ Pydantic trong FastAPI: Validation Dữ Liệu Dễ Dàng

pythonfastapipydanticvalidationapi
Làm Chủ Pydantic trong FastAPI: Validation Dữ Liệu Dễ Dàng

Pydantic là vũ khí bí mật khiến FastAPI trở nên mạnh mẽ. Nó xử lý data validation, serialization và type conversion tự động, đảm bảo API của bạn nhận và trả về dữ liệu sạch, đã được validate. Hãy cùng khám phá cách tận dụng Pydantic hiệu quả trong các ứng dụng FastAPI.

Pydantic Là Gì?

Pydantic là một thư viện data validation sử dụng Python type hints để validate dữ liệu. Nó là nền tảng của tính năng request validation tự động, response serialization và OpenAPI documentation generation của FastAPI.

Tại Sao Pydantic + FastAPI Hoàn Hảo:

  • Validation Tự Động - Dữ liệu không hợp lệ không bao giờ đến code của bạn
  • Type Safety - IDE autocomplete và type checking đầy đủ
  • Serialization - Chuyển đổi complex types sang JSON tự động
  • Documentation - Tạo OpenAPI schemas từ models
  • Performance - Validation nhanh sử dụng Rust (pydantic v2)
  • Developer Experience - Viết ít code hơn, có nhiều tính năng hơn

Pydantic Models Cơ Bản

Model Đơn Giản

from pydantic import BaseModel
 
class User(BaseModel):
    name: str
    email: str
    age: int
    is_active: bool = True  # Giá trị mặc định
 
# Sử dụng
user = User(name="Nguyễn Văn A", email="[email protected]", age=30)
print(user.name)  # "Nguyễn Văn A"
print(user.model_dump())  # Dict representation

Pydantic Làm Gì:

  • Validates types tự động
  • Chuyển đổi các types tương thích (ví dụ: "30" → 30)
  • Raise ValidationError cho dữ liệu không hợp lệ
  • Cung cấp JSON serialization

Tích Hợp FastAPI

from fastapi import FastAPI
from pydantic import BaseModel
 
app = FastAPI()
 
class User(BaseModel):
    name: str
    email: str
    age: int
 
@app.post("/users/")
async def create_user(user: User):
    # user đã được validated và typed
    return {"message": f"Created user {user.name}"}

Field Validation

Field Constraints

from pydantic import BaseModel, Field
 
class User(BaseModel):
    username: str = Field(
        ...,  # Field bắt buộc
        min_length=3,
        max_length=50,
        pattern="^[a-zA-Z0-9_]+$",
        description="Username alphanumeric"
    )
    age: int = Field(..., ge=0, le=150, description="Tuổi tính bằng năm")
    email: str = Field(..., description="Địa chỉ email hợp lệ")
    bio: str | None = Field(None, max_length=500)

Các Constraints Có Sẵn:

Cho Strings:

  • min_length / max_length - Giới hạn độ dài
  • pattern - Regex pattern
  • strip_whitespace - Xóa khoảng trắng đầu/cuối

Cho Numbers:

  • gt / ge - Greater than / greater or equal
  • lt / le - Less than / less or equal
  • multiple_of - Phải là bội số của giá trị

Cho Collections:

  • min_length / max_length - Kích thước collection
  • unique_items - Tất cả items phải unique

Built-in Validators

from pydantic import BaseModel, EmailStr, HttpUrl, UUID4
from datetime import datetime
 
class UserProfile(BaseModel):
    id: UUID4  # UUID validation
    email: EmailStr  # Email format validation
    website: HttpUrl  # URL validation
    created_at: datetime  # ISO datetime parsing
    updated_at: datetime | None = None

Custom Validators

Field Validators

from pydantic import BaseModel, field_validator
 
class User(BaseModel):
    username: str
    email: str
    password: str
    password_confirm: str
 
    @field_validator('username')
    @classmethod
    def username_alphanumeric(cls, v):
        if not v.isalnum():
            raise ValueError('Username phải là alphanumeric')
        return v
 
    @field_validator('email')
    @classmethod
    def email_lowercase(cls, v):
        return v.lower()
 
    @field_validator('password')
    @classmethod
    def password_strength(cls, v):
        if len(v) < 8:
            raise ValueError('Password phải có ít nhất 8 ký tự')
        if not any(c.isupper() for c in v):
            raise ValueError('Password phải chứa chữ hoa')
        if not any(c.isdigit() for c in v):
            raise ValueError('Password phải chứa số')
        return v

Model Validators

from pydantic import BaseModel, model_validator
 
class User(BaseModel):
    password: str
    password_confirm: str
    age: int
    parent_consent: bool = False
 
    @model_validator(mode='after')
    def check_passwords_match(self):
        if self.password != self.password_confirm:
            raise ValueError('Passwords không khớp')
        return self
 
    @model_validator(mode='after')
    def check_age_consent(self):
        if self.age < 18 and not self.parent_consent:
            raise ValueError('Người chưa thành niên cần sự đồng ý của phụ huynh')
        return self

Nested Models

Nesting Cơ Bản

from pydantic import BaseModel
from typing import List
 
class Address(BaseModel):
    street: str
    city: str
    country: str
    postal_code: str
 
class User(BaseModel):
    name: str
    email: str
    address: Address  # Nested model
 
# Sử dụng
user = User(
    name="Nguyễn Văn A",
    email="[email protected]",
    address={
        "street": "123 Đường ABC",
        "city": "Hà Nội",
        "country": "Việt Nam",
        "postal_code": "100000"
    }
)

Lists của Nested Models

class OrderItem(BaseModel):
    product_id: int
    quantity: int
    price: float
 
class Order(BaseModel):
    order_id: int
    items: List[OrderItem]  # List của nested models
    total: float
 
# Sử dụng
order = Order(
    order_id=1,
    items=[
        {"product_id": 1, "quantity": 2, "price": 10.0},
        {"product_id": 2, "quantity": 1, "price": 20.0}
    ],
    total=40.0
)

Optional và Union Types

Optional Fields

from pydantic import BaseModel
from typing import Optional
 
class User(BaseModel):
    name: str
    email: str
    phone: Optional[str] = None  # Có thể là None
    bio: str | None = None  # Python 3.10+ syntax

Union Types

from pydantic import BaseModel, Field
from typing import Union
 
class SuccessResponse(BaseModel):
    status: str = "success"
    data: dict
 
class ErrorResponse(BaseModel):
    status: str = "error"
    message: str
    code: int
 
# Union type - có thể là một trong hai
Response = Union[SuccessResponse, ErrorResponse]
 
@app.get("/data/")
async def get_data() -> Response:
    if some_condition:
        return SuccessResponse(data={"key": "value"})
    else:
        return ErrorResponse(message="Có lỗi xảy ra", code=500)

Model Configuration

Model Config

from pydantic import BaseModel, ConfigDict
 
class User(BaseModel):
    model_config = ConfigDict(
        str_strip_whitespace=True,  # Strip whitespace từ strings
        str_to_lower=True,  # Chuyển strings thành lowercase
        validate_assignment=True,  # Validate khi assignment
        validate_default=True,  # Validate default values
        extra='forbid',  # Cấm extra fields
        frozen=False,  # Cho phép mutation
    )
    
    name: str
    email: str

Xử Lý Extra Fields

# Cho phép extra fields
class FlexibleModel(BaseModel):
    model_config = ConfigDict(extra='allow')
    name: str
 
# Ignore extra fields (default)
class IgnoreExtraModel(BaseModel):
    model_config = ConfigDict(extra='ignore')
    name: str
 
# Cấm extra fields
class StrictModel(BaseModel):
    model_config = ConfigDict(extra='forbid')
    name: str
 
# Test
FlexibleModel(name="John", age=30)  # OK, lưu age
IgnoreExtraModel(name="John", age=30)  # OK, ignore age
StrictModel(name="John", age=30)  # ValidationError

Serialization và Deserialization

Model Dump

from pydantic import BaseModel
 
class User(BaseModel):
    name: str
    email: str
    password: str
 
user = User(name="John", email="[email protected]", password="secret")
 
# Chuyển sang dict
user_dict = user.model_dump()
 
# Exclude fields
public_data = user.model_dump(exclude={'password'})
 
# Include chỉ các fields cụ thể
minimal = user.model_dump(include={'name', 'email'})
 
# Chuyển sang JSON string
json_str = user.model_dump_json()
 
# Exclude unset fields
user_partial = User(name="John")
user_partial.model_dump(exclude_unset=True)  # Chỉ {'name': 'John'}

Response Models trong FastAPI

from fastapi import FastAPI
from pydantic import BaseModel
 
app = FastAPI()
 
class UserIn(BaseModel):
    username: str
    password: str
    email: str
 
class UserOut(BaseModel):
    username: str
    email: str
    # Password không included!
 
@app.post("/users/", response_model=UserOut)
async def create_user(user: UserIn):
    # Return UserIn, nhưng FastAPI serializes như UserOut
    return user  # Password tự động excluded

Settings và Configuration

Application Settings

from pydantic_settings import BaseSettings, SettingsConfigDict
 
class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file='.env',
        env_file_encoding='utf-8',
        case_sensitive=False
    )
    
    # Database
    database_url: str
    database_pool_size: int = 5
    
    # API Keys
    api_key: str
    secret_key: str
    
    # Features
    debug: bool = False
    log_level: str = "INFO"
 
# Load từ environment variables và .env file
settings = Settings()

Sử Dụng Settings trong FastAPI

from fastapi import FastAPI, Depends
from pydantic_settings import BaseSettings
from functools import lru_cache
 
class Settings(BaseSettings):
    app_name: str = "My API"
    database_url: str
    secret_key: str
 
@lru_cache()
def get_settings():
    return Settings()
 
app = FastAPI()
 
@app.get("/info/")
async def info(settings: Settings = Depends(get_settings)):
    return {"app_name": settings.app_name}

Ví Dụ Thực Tế: E-commerce Models

from pydantic import BaseModel, Field, field_validator, EmailStr
from typing import List, Optional
from datetime import datetime
from enum import Enum
 
# Enums
class OrderStatus(str, Enum):
    PENDING = "pending"
    PROCESSING = "processing"
    SHIPPED = "shipped"
    DELIVERED = "delivered"
    CANCELLED = "cancelled"
 
# Product Models
class ProductBase(BaseModel):
    name: str = Field(..., min_length=1, max_length=200)
    description: str = Field(..., max_length=2000)
    price: float = Field(..., gt=0, description="Giá tính bằng VND")
    stock: int = Field(..., ge=0)
    
    @field_validator('price')
    @classmethod
    def round_price(cls, v):
        return round(v, 2)
 
class Product(ProductBase):
    id: int
    sku: str
    is_active: bool = True
    created_at: datetime = Field(default_factory=datetime.now)
 
# Order Models
class OrderItem(BaseModel):
    product_id: int
    quantity: int = Field(..., ge=1)
    unit_price: float = Field(..., gt=0)
    
    @property
    def subtotal(self) -> float:
        return self.quantity * self.unit_price
 
class ShippingAddress(BaseModel):
    recipient_name: str
    street: str
    city: str
    postal_code: str
    country: str
    phone: str
 
class OrderBase(BaseModel):
    items: List[OrderItem] = Field(..., min_length=1)
    shipping_address: ShippingAddress
    
    @field_validator('items')
    @classmethod
    def validate_items(cls, v):
        if not v:
            raise ValueError('Đơn hàng phải có ít nhất một item')
        return v
    
    @property
    def total_amount(self) -> float:
        return sum(item.subtotal for item in self.items)
 
class Order(OrderBase):
    id: int
    user_id: int
    status: OrderStatus = OrderStatus.PENDING
    total: float
    created_at: datetime = Field(default_factory=datetime.now)

Best Practices

1. Sử Dụng Tên Field Mô Tả

# ❌ Không tốt
class User(BaseModel):
    n: str
    e: str
    a: int
 
# ✅ Tốt
class User(BaseModel):
    name: str
    email: str
    age: int

2. Cung Cấp Field Descriptions

# ✅ Luôn mô tả fields
class Product(BaseModel):
    name: str = Field(..., description="Tên sản phẩm")
    price: float = Field(..., description="Giá tính bằng VND", gt=0)
    stock: int = Field(..., description="Số lượng có sẵn", ge=0)

3. Sử Dụng Models Riêng Cho Input/Output

# ✅ Tách biệt concerns
class UserCreate(BaseModel):
    username: str
    password: str
    email: str
 
class UserUpdate(BaseModel):
    email: Optional[str] = None
    bio: Optional[str] = None
 
class UserOut(BaseModel):
    id: int
    username: str
    email: str
    # Không có password!

4. Validate Business Logic

# ✅ Sử dụng validators cho business rules
class Booking(BaseModel):
    check_in: datetime
    check_out: datetime
    guests: int = Field(..., ge=1, le=10)
    
    @model_validator(mode='after')
    def check_dates(self):
        if self.check_out <= self.check_in:
            raise ValueError('Check-out phải sau check-in')
        if (self.check_out - self.check_in).days > 30:
            raise ValueError('Thời gian ở tối đa là 30 ngày')
        return self

5. Sử Dụng Enums Cho Fixed Values

# ✅ Type-safe enums
from enum import Enum
 
class UserRole(str, Enum):
    ADMIN = "admin"
    USER = "user"
    GUEST = "guest"
 
class User(BaseModel):
    role: UserRole = UserRole.USER

Các Lỗi Phổ Biến

1. Mutable Default Arguments

# ❌ Không tốt - mutable default
class User(BaseModel):
    tags: list = []  # Shared across instances!
 
# ✅ Tốt - sử dụng default_factory
class User(BaseModel):
    tags: list = Field(default_factory=list)

2. Thiếu Required Fields

# ❌ Không tốt - không rõ nếu required
class User(BaseModel):
    name: str
    email: str = None  # Thực ra required nhưng trông optional
 
# ✅ Tốt - explicit
class User(BaseModel):
    name: str = Field(...)  # Required
    email: str = Field(...)  # Required
    bio: str | None = None  # Optional

Performance Tips

1. Sử Dụng Pydantic V2

Pydantic v2 được xây dựng với Rust và nhanh hơn đáng kể:

pip install "pydantic>=2.0"

2. Reuse Models

# ✅ Define một lần, sử dụng mọi nơi
class User(BaseModel):
    name: str
    email: str
 
# Đừng tạo lại cùng model nhiều lần

Kết Luận

Pydantic là xương sống của xử lý dữ liệu FastAPI, cung cấp:

  • Validation Tự Động - Bắt lỗi trước khi chúng đến code của bạn
  • Type Safety - Hỗ trợ IDE đầy đủ và type checking
  • Clear Errors - Thông báo lỗi validation chi tiết
  • Flexibility - Custom validators cho complex business logic
  • Performance - Validation nhanh với Pydantic v2
  • Developer Experience - Viết ít code hơn, có nhiều tính năng hơn

Bằng cách làm chủ Pydantic, bạn mở khóa toàn bộ tiềm năng của FastAPI. Sử dụng nó để xây dựng APIs mạnh mẽ, type-safe, validate dữ liệu tự động, serialize responses đúng cách và tạo tài liệu chính xác.

Bắt đầu xây dựng APIs tốt hơn với Pydantic ngay hôm nay - bản thân tương lai của bạn sẽ cảm ơn bạn! 🚀

Tham Khảo

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.