Memahami Pydantic models untuk validasi request dan response: model sederhana dan nested, constraint dengan Field, validator antar field dengan field_validator dan model_validator, hingga cara kerja mode validasi Pydantic 2.x.

Setelah di episode 4 kita menguasai routing dan parameter, sekarang kita masuk ke jantung FastAPI: Pydantic models. Di episode 2 kita sudah melihat bahwa semua request/response melewati Pydantic — sekarang saatnya mempelajari cara mendefinisikan schema yang benar.
Mengapa episode ini penting? Karena di sinilah kualitas API kalian ditentukan. API yang validasinya longgar akan menerima data sampah masuk ke database, sementara API yang validasinya berlebihan akan menyulitkan klien. Pydantic memberi kalian alat untuk menemukan titik tengah yang tepat — dan FastAPI menjadikannya bagian dari OpenAPI secara otomatis.
Pydantic model didefinisikan sebagai class Python yang mewarisi BaseModel. Di episode 5-8 kita memakai file schemas.py terpisah agar rapi:
from pydantic import BaseModel, Field
class ItemCreate(BaseModel):
name: str = Field(min_length=1, max_length=100)
price: float = Field(gt=0, le=1_000_000)
is_offer: bool = False
class Item(ItemCreate):
id: intDua model di atas memperlihatkan pola penting:
ItemCreate — schema untuk request body: name wajib (1-100 karakter), price wajib (harus 0 sampai 1.000.000), is_offer opsional berdefault False.Item — schema response: mewarisi semua field ItemCreate dan menambahkan id. Pewarisan model adalah cara idiomatis menghindari duplikasi antara input dan output.Field(...) menambahkan constraint. Singkatan yang perlu dihafal: gt (greater than), ge (greater or equal), lt, le, min_length, max_length, pattern.
from fastapi import FastAPI
from app.schemas import Item, ItemCreate
app = FastAPI()
@app.post("/items/")
def create_item(item: ItemCreate) -> Item:
item_id = 1
return Item(id=item_id, **item.model_dump())item: ItemCreate memberitahu FastAPI: request body harus sesuai ItemCreate. FastAPI lalu:
ItemCreate yang sudah bersih ke handler.Item (response model) sebelum serialisasi.model_dump() mengubah model menjadi dict (metode ini menggantikan dict() yang deprecated di Pydantic 2.x) — lalu kita bangun Item dengan id dari database.
Note
Perhatikan bahwa validasi dua arah: Pydantic tidak hanya memvalidasi request masuk, tetapi juga memvalidasi data yang kalian kembalikan. Ini mencegah bocornya field internal — misalnya password — ke response. Episode 7 membahas response model secara khusus.
API dunia nyata jarang datar. Pydantic mendukung model bertingkat secara native:
from pydantic import BaseModel, Field
class Tag(BaseModel):
name: str = Field(min_length=1, max_length=20)
class Order(BaseModel):
id: int
customer: str
items: list[Item]
tags: list[Tag] = []
metadata_: dict[str, str] = {}items: list[Item] — list berisi model lain — divalidasi elemen per elemen. JSON masuk:
{
"id": 10,
"customer": "Budi",
"items": [
{"id": 1, "name": "Keyboard", "price": 500000}
]
}divalidasi penuh: setiap item dicek terhadap Item, dan seluruh struktur wajib lengkap. Nested models ini yang akan dipakai terus menerus mulai episode 9 saat kita berhadapan dengan ORM.
Terkadang constraint sederhana tidak cukup — butuh logika validasi. Pydantic 2.x menyediakan validator per-field:
from pydantic import BaseModel, field_validator
class UserCreate(BaseModel):
username: str
email: str
@field_validator("username")
@classmethod
def username_berlaku(cls, v: str) -> str:
if not v.replace("_", "").isalnum():
raise ValueError("username hanya boleh alfanumerik dan underscore")
return v.lower()Dua hal yang wajib diingat soal validator Pydantic 2.x:
@classmethod (urutan dekorator: @field_validator di luar, @classmethod di dalam).raise ValueError(...) → Pydantic mengubahnya menjadi error validasi yang muncul sebagai detail 422.Perhatikan bahwa validator menghasilkan nilai baru (di sini v.lower()): pola normalisasi di dalam validator adalah teknik yang sangat berguna.
Kadang aturan validasi melibatkan beberapa field sekaligus. Contoh klasik: password dan konfirmasinya:
from pydantic import BaseModel, model_validator
class Signup(BaseModel):
password: str
confirm: str
@model_validator(mode="after")
@classmethod
def password_cocok(cls, data: "Signup") -> "Signup":
if data.password != data.confirm:
raise ValueError("password dan confirm harus sama")
return datamode="after" berarti validator berjalan setelah semua field divalidasi — jadi data sudah berupa objek Signup lengkap. Pola before (sebelum parsing) jarang dibutuhkan pemula; fokus pada after.
Tip
Aturan praktis: gunakan @field_validator untuk aturan satu field, dan @model_validator(mode="after") untuk aturan yang melihat beberapa field. Ini menjaga validator tetap kecil dan mudah diuji — sejalan dengan filosofi FastAPI yang high signal.
| Tanpa Pydantic (API naif) | Dengan Pydantic (API FastAPI) |
|---|---|
price bisa string "banyak" | Otomatis ditolak, detail field di 422 |
name bisa 10.000 karakter | max_length membatasi |
| Data sampah masuk ke database | Data bersih dan sudah dikonversi tipe |
| Tidak ada dokumentasi schema | Schema otomatis ada di /docs |
Inilah kenapa "validasi di boundary" adalah best practice keamanan: data yang masuk API adalah data asing, dan setiap asumsi soal tipe harus dibuktikan oleh validator — bukan dipercaya begitu saja.
| Pitfall | Solusi |
|---|---|
Memakai dict() Pydantic (deprecated) | Gunakan model_dump() |
Lupa @classmethod pada validator | Error deklarasi saat import |
| Menulis logika bisnis dalam validator | Validator untuk kebenaran data; logika bisnis di service layer |
| Mengganti nama field jadi keyword Python | Tambahkan underscore: metadata_ |
Inti yang harus dibawa pulang:
BaseModel; Field untuk constraint.Item(ItemCreate)) menghindari duplikasi schema.list[Item], dict[str, str].@field_validator untuk aturan satu field; @model_validator(mode="after") untuk lintas field.Di episode 6 selanjutnya kita akan membahas request body & forms — mengombinasikan body model dengan path/query params, memakai embed=True untuk struktur JSON tertentu, serta menangani data application/x-www-form-urlencoded dengan Form. Pastikan skema dari episode ini sudah dicoba, karena episode 6 membangun di atasnya!