Klien API bisa mengirim apa saja: field yang hilang, teks dua ribu karakter untuk kolom nama, atau email yang formatnya rusak. Parsing pada Chapter Request Parsing
hanya menjamin body bisa dibaca — bukan berarti isinya masuk akal. Pada Chapter ini kita membangun lapisan validasi sebagai fungsi murni yang mengembalikan daftar error, merancang bentuk error untuk status 422 yang mudah dibaca mesin, menolak field tak dikenal, dan mengenal konsep JSON Schema. Prinsipnya sama dengan validasi form HTML — yang berbeda hanya transport dan bentuk responsnya.
Validasi sebagai Fungsi Murni #
Validasi paling mudah dikembangkan dan diuji ketika dipisahkan dari HTTP. Alih-alih menebar http_response_code(422) dan echo di dalam setiap pemeriksaan, kita mengumpulkan seluruh logika ke satu fungsi validate() yang menerima input mentah beserta daftar aturan, lalu mengembalikan array error. Fungsi semacam ini bersifat murni: tidak mencetak apa pun, tidak menyentuh header, dan selalu mengembalikan data — sehingga bisa diuji langsung di CLI atau dengan PHPUnit tanpa server.
Aturan dinyatakan sebagai data: setiap field memetakan ke daftar aturan berbentuk string. Aturan sederhana seperti required berdiri sendiri, sedangkan aturan berparameter memakai titik dua, misalnya max:100 atau in:rendah,sedang,tinggi. Bentuk data seperti ini membuat daftar aturan mudah dibaca, dipindahkan ke berkas konfigurasi, bahkan dibangkitkan otomatis dari definisi skema.
<?php
function checkRule(string $field, mixed $value, string $rule, ?string $param): ?string
{
return match ($rule) {
"required" => ($value === null || $value === "" || $value === [])
? "Field $field wajib diisi"
: null,
"string" => ($value !== null && !is_string($value))
? "Field $field harus berupa string"
: null,
"max" => (is_string($value) && mb_strlen($value) > (int) $param)
? "Field $field maksimal $param karakter"
: null,
"email" => (is_string($value) && filter_var($value, FILTER_VALIDATE_EMAIL) === false)
? "Field $field harus berupa email yang valid"
: null,
"in" => (is_string($value) && !in_array($value, explode(",", (string) $param), true))
? "Field $field harus salah satu dari: $param"
: null,
default => null,
};
}
function validate(array $input, array $rules): array
{
$errors = [];
foreach ($rules as $field => $fieldRules) {
$value = $input[$field] ?? null;
foreach ($fieldRules as $rule) {
[$name, $param] = array_pad(explode(":", $rule, 2), 2, null);
$message = checkRule($field, $value, $name, $param);
if ($message !== null) {
$errors[] = [
"field" => $field,
"rule" => $name,
"message" => $message,
];
break;
}
}
}
return $errors;
}
$rules = [
"title" => ["required", "string", "max:100"],
"assignee" => ["required", "email"],
"priority" => ["in:rendah,sedang,tinggi"],
];
$valid = validate([
"title" => "Rilis API v2",
"assignee" => "[email protected]",
"priority" => "tinggi",
], $rules);
echo "Input sehat: " . ($valid === [] ? "lolos validasi" : "ditolak") . "\n";
$invalid = validate([
"assignee" => "budi-di-kantor",
"priority" => "segera",
], $rules);
foreach ($invalid as $error) {
echo "- {$error['field']} ({$error['rule']}): {$error['message']}\n";
}Output yang Dihasilkan:
Input sehat: lolos validasi
- title (required): Field title wajib diisi
- assignee (email): Field assignee harus berupa email yang valid
- priority (in): Field priority harus salah satu dari: rendah,sedang,tinggiMari kita bedah kode di atas:
checkRule(): menilai satu aturan untuk satu nilai memakaimatch— mengembalikan pesan error bila gagal, ataunullbila lolos. Setiap aturan menjaga dirinya sendiri dari nilainulldengan pemeriksaanis_string(), sehingga field opsional yang tidak dikirim tidak memicu error palsu.validate(): berkeliling ke setiap field, lalu mencoba aturannya satu per satu. Pernyataanbreaksetelah error pertama memastikan satu field hanya melaporkan satu masalah — daftar pesan tidak menumpuk dan klien bisa memperbaiki bertahap.explode(":", $rule, 2): memisahkan nama aturan dari parameternya; batas2membuat parameter boleh mengandung titik dua tambahan.- Array
$errorsyang kosong ([]) berarti seluruh field lolos — sinyal bersih yang mudah dites dengan=== [].
Perhatikan field priority: aturannya tidak memuat required, jadi field tersebut opsional. Nilai null tidak pernah sampai ke aturan in karena pemeriksaan is_string() menghentikannya lebih dulu; nilai yang terkirim ("segera") baru gagal karena tidak ada dalam daftar yang diizinkan.
Menolak Field Tak Dikenal #
Klien yang salah ketik prioritas alih-alih priority akan diam-diam lolos dari validasi di atas — aturan priority tidak menemukan nilainya, sementara field asing tidak pernah diperiksa. Bug semacam ini sangat sulit dilacak karena API tampak sehat padahal data penting tidak pernah tersimpan. Solusinya adalah mendata field yang diizinkan dan menolak sisanya secara eksplisit:
<?php
function findUnknownFields(array $input, array $allowedFields): array
{
return array_values(array_diff(array_keys($input), $allowedFields));
}
$allowedFields = ["title", "assignee", "priority"];
$body = [
"title" => "Review dokumentasi API",
"assignee" => "[email protected]",
"prioritas" => "tinggi",
];
$unknown = findUnknownFields($body, $allowedFields);
if ($unknown === []) {
echo "Semua field dikenal\n";
} else {
echo "Field tak dikenal: " . implode(", ", $unknown) . "\n";
}Output yang Dihasilkan:
Field tak dikenal: prioritasFungsi findUnknownFields() membandingkan kunci input dengan daftar field resmi lewat array_diff(), lalu array_values() merapikan indeks hasilnya. Dalam handler, temuan ini sebaiknya dikembalikan sebagai error 422 dengan aturan unknown_field supaya klien langsung menyadari salah ketiknya. Penolakan field asing sekaligus menutup celah mass assignment: klien tidak bisa menyelundupkan field seperti role atau is_admin yang bukan urusannya di endpoint tersebut.
Bentuk Error untuk Status 422 #
Status 422 Unprocessable Content adalah tempat error validasi berada — bukan 400, karena request-nya sendiri terparse dengan baik (pembahasan lengkap ada di Chapter Status Codes dan Error Responses
). Yang menentukan kualitas API bukan hanya statusnya, tetapi bentuk detail errornya. Kita pakai bentuk yang bisa dibaca mesin: setiap elemen details memuat field untuk menunjuk input yang salah, rule sebagai kode programatik, dan message sebagai teks untuk manusia.
<?php
function respond(int $status, array $payload): array
{
return [
$status,
["Content-Type: application/json; charset=utf-8"],
json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
];
}
function registerHandler(array $request): array
{
$body = $request["body"] ?? null;
if (!is_array($body)) {
return respond(400, [
"error" => ["code" => "INVALID_BODY", "message" => "Body bukan JSON objek valid"],
]);
}
$errors = [];
$name = $body["name"] ?? null;
if (!is_string($name) || trim($name) === "") {
$errors[] = [
"field" => "name",
"rule" => "required",
"message" => "Field name wajib diisi",
];
}
$email = $body["email"] ?? null;
if (!is_string($email) || filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
$errors[] = [
"field" => "email",
"rule" => "email",
"message" => "Field email harus berupa email yang valid",
];
}
$password = $body["password"] ?? null;
if (!is_string($password) || mb_strlen($password) < 8) {
$errors[] = [
"field" => "password",
"rule" => "min",
"message" => "Field password minimal 8 karakter",
];
}
if ($errors !== []) {
return respond(422, [
"error" => [
"code" => "VALIDATION_ERROR",
"message" => "Request tidak lolos validasi",
"details" => $errors,
],
]);
}
return respond(201, [
"data" => ["id" => 9, "name" => trim($name), "email" => $email],
]);
}
$request = [
"method" => "POST",
"path" => "/register",
"query" => [],
"headers" => ["Content-Type" => "application/json"],
"body" => [
"name" => " ",
"email" => "siti#example.com",
"password" => "rahasia",
],
];
[$status, $headers, $responseBody] = registerHandler($request);
echo "HTTP " . $status . "\n";
echo $responseBody . "\n";Output yang Dihasilkan:
HTTP 422
{"error":{"code":"VALIDATION_ERROR","message":"Request tidak lolos validasi","details":[{"field":"name","rule":"required","message":"Field name wajib diisi"},{"field":"email","rule":"email","message":"Field email harus berupa email yang valid"},{"field":"password","rule":"min","message":"Field password minimal 8 karakter"}]}}Handler di atas membentuk tiga nasib request: 400 saat body bukan array (urusan parsing), 422 saat detail validasi berisi (urusan validasi), dan 201 saat semua field sehat. Pemeriksaan manual di sini sengaja ditulis eksplisit agar contoh berdiri sendiri — pada proyek nyata, blok pemeriksaan tersebut tinggal digantikan satu baris pemanggilan validate() dari contoh pertama, dan bentuk details-nya tetap sama. Aplikasi mobile yang menerima respons seperti ini bisa langsung menandai kolom email di layarnya tanpa harus memparsing teks bebas.
Konsep JSON Schema #
Menulis aturan sebagai array PHP sudah cukup rapi, tetapi komunitas punya bentuk standar yang lebih umum: JSON Schema. Skema mendeskripsikan bentuk data JSON secara deklaratif — properti apa saja yang diharapkan, tipenya, batas panjangnya, nilai yang diizinkan, dan field wajibnya. Berikut skema untuk input task yang persis menerjemahkan aturan kita di atas:
<?php
$schemaJson = <<<'JSON'
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "TaskInput",
"type": "object",
"properties": {
"title": {"type": "string", "minLength": 1, "maxLength": 100},
"assignee": {"type": "string", "format": "email"},
"priority": {"enum": ["rendah", "sedang", "tinggi"]}
},
"required": ["title", "assignee"],
"additionalProperties": false
}
JSON;
var_dump(json_validate($schemaJson));Output yang Dihasilkan:
bool(true)Skema ini membaca hal yang sama dengan kode kita: required adalah daftar field wajib, maxLength adalah aturan max:100, dan enum adalah aturan in. Kunci additionalProperties: false adalah padanan findUnknownFields() — skema menolak kehadiran field di luar properties. Fungsi json_validate() dari PHP 8.3 di sini hanya memastikan string skema memang JSON yang sah; memvalidasi request terhadap skema memerlukan library JSON Schema dari Composer. Ide besarnya: satu berkas skema bisa menjadi sumber aturan validasi, dokumentasi, hingga pengecekan di sisi klien.
Validasi API dan Validasi Form HTML #
Jika Anda pernah membaca Section Form Handling dan Validation
, seluruh pola di Chapter ini pasti terasa familier — dan memang seharusnya begitu. Prinsip validasi server-side
berlaku identik: server adalah otoritas tunggal, dan aturan yang sama ditulis ulang untuk tiap sumber input. Bedanya hanya transport dan bentuk masukan: form datang sebagai application/x-www-form-urlencoded dari browser, sedangkan API menerima JSON terstruktur dari program lain, sehingga pemeriksaan tipe berupa is_string() menggantikan kebiasaan memfilter $_POST.
Perbedaan kedua ada di media pelaporan. Form HTML menampilkan pesan error di samping kolom input untuk manusia; API harus menstrukturnya (field, rule, message) agar program klien bisa bereaksi otomatis. Pembedaan antara membersihkan data dan menilainya juga tetap relevan — topik sanitasi vs validasi
berlaku sama persis untuk body JSON.
Ringkasan #
Validasi request yang sehat punya tiga ciri: aturan berbentuk data yang dievaluasi fungsi murni, error 422 terstruktur dengan details berisi field/rule/message, dan penolakan eksplisit terhadap field asing. Konsep JSON Schema menunjukkan arah lanjutannya bila aturan tumbuh kompleks. Dengan request yang masuk kini bisa dipercaya, kita berbalik ke masalah di sisi sebaliknya: respons daftar yang membengkak. Ketika resource bertumbuh menjadi ribuan baris, endpoint butuh pagination, filtering, dan sorting — fokus Chapter berikutnya.