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 representationPydantic 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àipattern- Regex patternstrip_whitespace- Xóa khoảng trắng đầu/cuối
Cho Numbers:
gt/ge- Greater than / greater or equallt/le- Less than / less or equalmultiple_of- Phải là bội số của giá trị
Cho Collections:
min_length/max_length- Kích thước collectionunique_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 = NoneCustom 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 vModel 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 selfNested 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+ syntaxUnion 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: strXử 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) # ValidationErrorSerialization 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 excludedSettings 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: int2. 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 self5. 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.USERCá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 # OptionalPerformance 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ầnKế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
- Pydantic Documentation: https://docs.pydantic.dev/
- Pydantic V2 Migration: https://docs.pydantic.dev/latest/migration/
- FastAPI Pydantic Guide: https://fastapi.tiangolo.com/tutorial/body/
- Pydantic Settings: https://docs.pydantic.dev/latest/concepts/pydantic_settings/
- GitHub: https://github.com/pydantic/pydantic
📬 Đă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.