Nhiều API FastAPI lưu cả những trường hoàn toàn có thể tính ra được — số đêm, tổng tiền — rồi tự tay tính lại ở endpoint, ở service và ở frontend. Chỉ cần một chỗ quên cập nhật, dữ liệu lệch nhau ngay. Với Pydantic v2, bạn không cần lưu hay tính tay những trường đó: dùng @computed_field để trường tự suy ra từ các trường khác, và @model_validator(mode="after") để kiểm tra chéo toàn bộ model — áp dụng khi một trường chỉ có nghĩa khi đặt cạnh trường khác, như ngày kết thúc phải sau ngày bắt đầu.
Vấn đề
Giả sử bạn làm API đặt phòng. Client gửi ngày bắt đầu, ngày kết thúc và giá mỗi đêm, nhưng bạn còn lưu thêm số đêm và tổng tiền, rồi tính chúng trong endpoint. Cách này dư thừa, dễ lệch nhau khi một chỗ sửa mà chỗ khác quên, và logic validate ngày nằm rải rác thay vì tập trung ở model. Tệ hơn, nếu client gửi kèm số đêm sai, server có thể tin và lưu luôn con số đó.
# Cách dễ lệch: tính tay trong endpoint, không có gì chặn ngày sai@app.post("/bookings")def create_booking(data: dict): nights = (data["end_date"] - data["start_date"]).days total = nights * data["price_per_night"] return {"nights": nights, "total": total}
Đoạn code trên chưa chặn ngày kết thúc trước ngày bắt đầu — số đêm âm, tổng tiền âm vẫn trả về bình thường.
Giải pháp
Đưa toàn bộ logic suy ra và kiểm tra chéo vào chính Pydantic model. Trường tính toán được đánh dấu bằng @computed_field, còn kiểm tra chéo dùng @model_validator(mode="after") — chế độ “after” chạy sau khi từng trường đã validate xong, nên bạn làm việc với model hoàn chỉnh, không phải dữ liệu thô.
from datetime import datefrom pydantic import BaseModel, Field, computed_field, model_validatorclass BookingCreate(BaseModel): start_date: date end_date: date price_per_night: float = Field(gt=0) @computed_field @property def nights(self) -> int: return (self.end_date - self.start_date).days @computed_field @property def total(self) -> float: return round(self.nights * self.price_per_night, 2) @model_validator(mode="after") def check_dates(self): if self.end_date <= self.start_date: raise ValueError("end_date phải sau start_date") return self
Khi dùng model này trong endpoint FastAPI, mọi thứ tự vận hành: client gửi ba trường đầu vào, response tự kèm nights và total đã tính đúng, còn cặp ngày sai bị chặn ngay từ cổng với lỗi 422 mà không cần thêm dòng kiểm tra nào trong endpoint. Một điểm đáng chú ý: trường tính toán không phải trường đầu vào — nếu client cố gửi kèm nights sai, Pydantic vẫn tính lại từ ngày thật. Validator bắt buộc phải trả về self, nếu quên, model sẽ bị thay bằng None và lỗi rất khó đoán.
from fastapi import FastAPIapp = FastAPI()@app.post("/bookings", status_code=201)def create_booking(data: BookingCreate): # data.nights và data.total đã có sẵn, đã validate xong return data
Áp dụng vào dự án của bạn
Quy tắc phân biệt rất đơn giản: nếu một giá trị luôn suy ra được từ các trường khác trong cùng model, hãy để @computed_field tính, đừng lưu hay tính tay. Nếu một quy tắc chỉ đúng khi xét nhiều trường cùng lúc — ngày kết thúc sau ngày bắt đầu, mật khẩu nhập lại phải khớp — hãy đặt nó vào @model_validator(mode="after"). Ngược lại, kiểm tra chỉ liên quan một trường, như giá phải lớn hơn 0, vẫn thuộc về Field hoặc @field_validator. Giữ computed field thuần túy — chỉ tính toán, không gọi database hay API ngoài.
Bài viết liên quan
- FastAPI: validate tham số đường dẫn bằng Path() — chặn ID sai ngay từ cổng API trong 5 phút
- FastAPI: viết dependency dạng class — gom tham số chung và logic tái sử dụng trong 5 phút
- FastAPI: gom cả chục query param vào một Pydantic model — validate tập trung trong 5 phút
Câu hỏi thường gặp
computed_field khác gì @property thường?
@property thường không xuất hiện trong model_dump() hay response JSON của FastAPI, còn @computed_field thì có — client nhận được trường tính toán như một trường bình thường trong response.
Client có thể gửi kèm trường computed để ghi đè không?
Không. Trường tính toán không phải trường đầu vào, nên giá trị client gửi kèm bị bỏ qua và Pydantic tính lại từ dữ liệu thật.
mode=”before” và mode=”after” khác nhau thế nào?
Chế độ “before” nhận dữ liệu thô trước khi validate, phù hợp để chuẩn hóa đầu vào. Chế độ “after” nhận model đã validate xong, phù hợp để kiểm tra chéo — đó là chế độ cần cho ví dụ ngày tháng ở trên.
Khi nào dùng model_validator thay vì field_validator?
Dùng @field_validator khi quy tắc chỉ liên quan một trường. Dùng @model_validator khi quy tắc cần so sánh nhiều trường với nhau, vì lúc đó bạn mới có toàn bộ model trong tay.
💬 Bạn đã thử cách này trong dự án của mình chưa? Nếu gặp lỗi hoặc có cách làm gọn hơn, để lại bình luận bên dưới — mình đọc hết và sẽ bổ sung những ý hay vào bài.
Nguồn tham khảo
- Pydantic Documentation — Computed fields: https://docs.pydantic.dev/latest/concepts/fields/#computed-fields
- Pydantic Documentation — Model validators: https://docs.pydantic.dev/latest/concepts/validators/#model-validators
- FastAPI Documentation — Body: https://fastapi.tiangolo.com/tutorial/body/
