Endpoint POST pada chapter sebelumnya menerima body dict mentah — JSON tanpa field judul, dengan selesai berupa string, bahkan body kosong, semuanya diterima tanpa protes. Input yang kotor seperti ini akan merembet ke penyimpanan, merusak data, dan baru meledak sebagai bug di sisi klien. Yang kita butuhkan adalah kontrak tegas di pintu masuk: struktur field, tipe, dan batasan nilai yang diperiksa sebelum fungsi rute dijalankan. FastAPI menyerahkan tugas ini ke library Pydantic (versi 2.12 terpasang di lingkungan pengujian): kita mendeklarasikan sebuah class BaseModel, dan setiap request body yang tidak sesuai ditolak otomatis dengan status 422.
BaseModel sebagai Kontrak Request #
Request model dideklarasikan seperti class Python biasa dengan type hints — materi yang berakar di Section Type Hints dan Static Analysis:
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
class TugasBaru(BaseModel):
judul: str = Field(min_length=3, max_length=100)
prioritas: int = Field(default=1, ge=1, le=5)
catatan: str | None = None
app = FastAPI()
@app.post("/tugas", status_code=201)
def buat_tugas(tugas: TugasBaru):
return {"id": 1, **tugas.model_dump()}
client = TestClient(app)
r = client.post("/tugas", json={"judul": "Belajar Pydantic"})
print(r.status_code, r.json())
r = client.post("/tugas", json={"judul": "Belajar REST", "prioritas": 2, "catatan": "Sore ini"})
print(r.status_code, r.json())Output yang Dihasilkan:
201 {'id': 1, 'judul': 'Belajar Pydantic', 'prioritas': 1, 'catatan': None}
201 {'id': 1, 'judul': 'Belajar REST', 'prioritas': 2, 'catatan': 'Sore ini'}Mari kita telaah perannya satu per satu:
- Baris 6 (
class TugasBaru(BaseModel)): class Pydantic menggantikandictmentah pada chapter sebelumnya sebagai tipe parametertugas; FastAPI mem-parse, memvalidasi, dan mengonversi body JSON menjadi instance class ini. - Baris 7 (
judul: str = Field(...)): field tanpa nilai default berarti wajib — request tanpajudulditolak. - Baris 8 (
prioritas: int = Field(default=1, ...)): field boleh dilewatkan klien; nilai1terisi otomatis, terlihat pada output pertama. - Baris 9 (
catatan: str | None = None): pola Optional standar — hadir sebagaiNonesaat tidak dikirim. - Baris 15 (
@app.post("/tugas", status_code=201)): parameterstatus_codemengubah baris status dari default200menjadi201 Created— aturan kapan tiap kode dipakai dibahas di chapter Status Codes dan Error Responses. - Baris 17 (
tugas.model_dump()): method bawaan Pydantic mengubah instance menjadi dictionary — berguna saat kita perlu menggabungkannya dengan field lain sepertiid.
Field Wajib, Default, dan Optional #
Ketiga status field di atas punya makna yang berbeda dan tidak saling menggantikan:
| Deklarasi | Makna | Jika tidak dikirim klien |
|---|---|---|
judul: str | wajib, tidak boleh None | 422 |
prioritas: int = Field(default=1, ...) | opsional dengan nilai default | terisi 1 |
catatan: str | None = None | opsional dan boleh kosong | terisi None |
Perbedaan ketiganya sering membuat bingung: nilai default mengisi field yang hilang, sedangkan str | None juga mengizinkan klien mengirim "catatan": null secara eksplisit. Pilih str | None = None hanya saat None memang nilai yang bermakna.
Constraint Validasi dan Error 422 #
Field() tidak hanya mengatur default — parameter constraint-nya membatasi nilai yang diterima: min_length/max_length untuk string dan collection, gt/ge/lt/le untuk angka. Mari kita kirim body yang melanggar kontrak:
r = client.post("/tugas", json={"prioritas": 2})
print(r.status_code, r.json())
r = client.post("/tugas", json={"judul": "OK", "prioritas": 9})
print(r.status_code, r.json())Output yang Dihasilkan:
422 {'detail': [{'type': 'missing', 'loc': ['body', 'judul'], 'msg': 'Field required', 'input': {'prioritas': 2}}]}
422 {'detail': [{'type': 'string_too_short', 'loc': ['body', 'judul'], 'msg': 'String should have at least 3 characters', 'input': 'OK', 'ctx': {'min_length': 3}}, {'type': 'less_than_equal', 'loc': ['body', 'prioritas'], 'msg': 'Input should be less than or equal to 5', 'input': 9, 'ctx': {'le': 5}}]}Kedua response berstatus 422 Unprocessable Entity — kode yang dipilih FastAPI untuk “body diterima tapi gagal validasi”. Setiap item pada list detail memuat empat kunci: type (jenis pelanggaran), loc (lokasi field, misalnya ['body', 'judul']), msg (pesan untuk manusia), dan input (nilai yang ditolak). Perhatikan bahwa response kedua memuat dua pelanggaran sekaligus — judul terlalu pendek dan prioritas melebihi 5 — dikumpulkan dalam satu penolakan, bukan dihentikan di pelanggaran pertama.
Validasi ini juga terdokumentasi sendiri: constraint min_length=3 pada contoh di atas muncul sebagai minLength di skema OpenAPI dan halaman /docs — kita lihat caranya di chapter OpenAPI dan API Documentation. Sebelum itu, chapter Response Models mengurus sisi keluaran agar field internal tidak ikut bocor ke klien.