Handler sudah memegang data hasil parsing request, dan kini tiba waktunya membalas klien. Di sisi respons, tugas kita kebalikan dari parsing: mengubah array dan object PHP menjadi JSON — proses yang disebut serialization. Bukan sekadar memanggil json_encode() lalu selesai; flag yang dipilih, format waktu, dan field yang disembunyikan menentukan apakah API Anda nyaman dikonsumsi atau justru mengejutkan klien di kemudian hari. Pada Chapter ini kita susun lapisan serialization yang rapi mulai dari flag dasar sampai perlindungan field sensitif.
Dari Array PHP ke JSON #
Fungsi json_encode() punya banyak flag, dan pilihan defaultnya konservatif: karakter non-ASCII di-escape menjadi \uXXXX dan garis miring menjadi \/. Untuk API berbahasa apa pun, kombinasi flag berikut adalah titik awal terbaik:
<?php
$task = [
"title" => "Rapat tim — Q1 2026",
"url" => "https://example.com/tasks/7",
];
echo json_encode($task) . "\n";
echo json_encode($task, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
echo json_encode($task, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n";Output yang Dihasilkan:
{"title":"Rapat tim \u2014 Q1 2026","url":"https:\/\/example.com\/tasks\/7"}
{"title":"Rapat tim — Q1 2026","url":"https://example.com/tasks/7"}
{
"title": "Rapat tim — Q1 2026",
"url": "https://example.com/tasks/7"
}Perbedaan tiga baris output tersebut persis seperti yang diharapkan:
- Baris pertama (tanpa flag): em dash menjadi
\u2014dan URL menjadihttps:\/\/example.com\/tasks\/7. JSON hasilnya tetap valid, tetapi tidak nyaman dibaca manusia dan sedikit lebih panjang. - Baris kedua (
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES): karakter UTF-8 ditulis apa adanya dan garis miring tidak di-escape. Inilah format produksi yang kita pakai konsisten di seluruh section ini. - Baris ketiga (+
JSON_PRETTY_PRINT): indentasi empat spasi ditambahkan. Ini berguna untuk log dan debugging.
| Flag | Efek | Kapan dipakai |
|---|---|---|
JSON_UNESCAPED_UNICODE | Karakter non-ASCII tampil apa adanya | Selalu (produksi) |
JSON_UNESCAPED_SLASHES | / tidak menjadi \/ | Selalu (produksi) |
JSON_PRETTY_PRINT | Indentasi dan baris baru | Hanya dev/debug |
Kelas Serializer sebagai Abstraksi #
Menuliskan flag dan penanganan error di setiap handler adalah resep duplikasi. Solusinya adalah satu kelas JsonSerializer yang menjadi satu-satunya tempat kebijakan serialization ditetapkan — flag default, kedalaman maksimum, dan cara merespons kegagalan:
<?php
final class JsonSerializer
{
public function __construct(
private readonly int $flags = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES,
private readonly int $depth = 512,
) {
}
public function serialize(array $data): string
{
$json = json_encode($data, $this->flags, $this->depth);
if ($json === false) {
throw new RuntimeException(
"Serialisasi JSON gagal: " . json_last_error_msg()
);
}
return $json;
}
}
$serializer = new JsonSerializer();
echo $serializer->serialize(["title" => "Belajar serialisasi", "done" => false]) . "\n";
echo $serializer->serialize(["stats" => ["total" => 12, "selesai" => 5]]) . "\n";Output yang Dihasilkan:
{"title":"Belajar serialisasi","done":false}
{"stats":{"total":12,"selesai":5}}Constructor kelas ini memakai constructor property promotion dengan nilai default, sehingga new JsonSerializer() saja sudah cukup untuk kebijakan standar. Inti perilakunya ada di pemeriksaan $json === false: json_encode() yang gagal tidak melempar exception, melainkan mengembalikan false dan menyimpan alasannya via json_last_error(). Dengan mengubah kegagalan senyap itu menjadi RuntimeException, pemanggil tidak pernah tanpa sadar mengirim string kosong atau false kepada klien. Handler tinggal menangkap exception tersebut di lapisan error handler dan membalas 500 — pola yang akan kita rampungkan di Chapter Status Codes dan Error Responses
.
Format Waktu yang Konsisten #
Tanggal adalah sumber inkonsistensi klasik API: satu endpoint mengirim 15-01-2026, endpoint lain 2026/01/15 08:00 WIB. Aturan yang menyelamatkan: selalu kirim waktu dalam format ISO 8601 (konstanta DateTimeImmutable::ATOM) dan selalu dalam zona waktu UTC. Fungsi berikut menegakkan keduanya sekaligus:
<?php
function formatDateTime(DateTimeImmutable $dateTime): string
{
return $dateTime
->setTimezone(new DateTimeZone("UTC"))
->format(DateTimeImmutable::ATOM);
}
$createdAt = new DateTimeImmutable(
"2026-01-15 08:00:00",
new DateTimeZone("Asia/Jakarta")
);
echo formatDateTime($createdAt) . "\n";Output yang Dihasilkan:
2026-01-15T01:00:00+00:00Waktu 2026-01-15 08:00:00 zona Asia/Jakarta dikonversi menjadi 2026-01-15T01:00:00+00:00 — pukul delapan pagi WIB memang pukul satu malam UTC. Rantai setTimezone() lalu format() berjalan pada DateTimeImmutable, sehingga objek asli tidak berubah dan aman dipakai ulang. Format ISO 8601 dengan offset eksplisit seperti ini bisa diurai langsung oleh new DateTimeImmutable($string) di sisi klien, oleh Date di JavaScript, dan oleh hampir semua pustaka tanggal modern. Kebijakan zona waktu dan pelokalan yang lebih luas sudah kita bahas tuntas di Section Date Time dan Localization
.
Menyembunyikan Field Sensitif #
Baris database hampir selalu berisi kolom yang tidak boleh keluar dari server: password_hash, token API, kredensial internal. Kesalahan fatal terjadi ketika baris mentah di-encode tanpa saringan. Pendekatan paling aman adalah whitelist — daftar eksplisit field yang boleh tampil — bukan blacklist field terlarang yang gampang kecolongan saat kolom baru ditambahkan:
<?php
final class UserSerializer
{
private const PUBLIC_FIELDS = ["id", "name", "email", "createdAt"];
public static function toArray(array $user): array
{
return array_intersect_key($user, array_flip(self::PUBLIC_FIELDS));
}
}
$user = [
"id" => 7,
"name" => "Siti Rahma",
"email" => "[email protected]",
"passwordHash" => '$2y$10$contohhashyangsangatpanjang',
"apiKey" => "YOUR_API_KEY",
"createdAt" => "2026-01-15T01:00:00+00:00",
];
echo json_encode(
UserSerializer::toArray($user),
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
) . "\n";Output yang Dihasilkan:
{"id":7,"name":"Siti Rahma","email":"[email protected]","createdAt":"2026-01-15T01:00:00+00:00"}Kunci mekanismenya ada di array_intersect_key($user, array_flip(self::PUBLIC_FIELDS)): array_flip() membalik daftar nama field menjadi kunci array, lalu array_intersect_key() hanya menyimpan elemen $user yang kuncinya ada di daftar itu. Hasil output membuktikan passwordHash dan apiKey tidak pernah sampai ke JSON, padahal keduanya ada di data sumber. Konstanta PUBLIC_FIELDS dideklarasikan private const karena daftar ini adalah detail internal class — pemanggil hanya melihat method toArray(). Setiap resource sebaiknya punya class semacam ini (TaskSerializer, OrderSerializer) agar keputusan “field apa yang publik” hidup di satu tempat yang mudah diaudit; praktik rahasia kredensial secara umum dibahas di Chapter Secrets Management
.
Kedalaman Nesting dan Kegagalan Encoding #
json_encode() membatasi kedalaman struktur yang di-encode — defaultnya 512 level. Data yang dihasilkan rekursi tanpa kendali bisa menembus batas itu, dan seperti ditunjukkan kelas serializer tadi, kegagalannya senyap. Contoh berikut membangun array bersarang 600 level untuk melihat kejadiannya secara langsung:
<?php
$nested = "nilai paling dalam";
for ($i = 0; $i < 600; $i++) {
$nested = ["level" => $nested];
}
$json = json_encode($nested, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($json === false) {
echo "kode error: " . json_last_error() . "\n";
echo "pesan: " . json_last_error_msg() . "\n";
}Output yang Dihasilkan:
kode error: 1
pesan: Maximum stack depth exceededLoop tersebut membungkus string menjadi array 600 tingkat, melebihi jatah 512. json_encode() mengembalikan false dengan kode error 1 (JSON_ERROR_DEPTH) dan pesan Maximum stack depth exceeded. Kegagalan encoding lain yang serupa: string byte yang bukan UTF-8 valid memicu JSON_ERROR_UTF8, dan nilai NAN/INF memicu JSON_ERROR_INF_AND_NAN. Dalam praktik, kedalaman berlebih biasanya gejala data rekursif — dua entity yang saling menunjuk lewat array referensi. Solusinya bukan menaikkan batas kedalaman, melainkan memotong relasi di serializer (misalnya TaskSerializer tidak ikut menyertakan UserSerializer secara penuh, cukup assigneeId).
Header Content-Type dan Helper Respons #
JSON yang baik harus diumumkan sebagai JSON. Header Content-Type: application/json; charset=utf-8 memberi tahu klien cara memparse body tanpa menebak; menyebut charset=utf-8 sekaligus menegaskan encoding yang kita pilih di flag. Sebagai penutup, berikut helper yang merangkai status, header, dan body JSON dalam satu nilai balik — pasangan alami dari pola [status, headers, body] yang kita pakai sejak chapter routing:
<?php
function respondJson(int $status, array $payload): array
{
return [
$status,
["Content-Type: application/json; charset=utf-8"],
json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
];
}
[$status, $headers, $body] = respondJson(200, ["data" => ["id" => 7, "title" => "Review PR"]]);
echo "Status: " . $status . "\n";
echo "Header: " . $headers[0] . "\n";
echo "Body: " . $body . "\n";Output yang Dihasilkan:
Status: 200
Header: Content-Type: application/json; charset=utf-8
Body: {"data":{"id":7,"title":"Review PR"}}Helper ini sengaja tetap berbentuk array, bukan langsung memanggil header() dan echo, demi testabilitas yang sama dengan handler lain. Di sisi server nyata, glue code-lah yang menerjemahkan array ini menjadi HTTP sungguhan: http_response_code($status), loop header() untuk setiap elemen, lalu echo $body. Dengan semua bahan ini — flag yang konsisten, kelas serializer, format waktu, whitelist field, dan helper respons — kita siap merakit handler API utuh yang menyerap request dan memancarkan JSON dalam satu putaran penuh, pembahasan Chapter JSON APIs
.
Ringkasan #
Serialization yang disiplin berarti: flag JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES untuk produksi, satu kelas serializer sebagai pemegang kebijakan, waktu selalu ISO 8601 UTC, dan field sensitif disaring lewat whitelist — bukan dikirim apa adanya dari baris database. Kegagalan json_encode() harus diubah dari false senyap menjadi exception yang terdengar. Lapisan-lapisan ini terasa sederhana sendiri-sendiri, tetapi dirangkai menjadi fondasi API yang bentuk responsnya bisa dipercaya klien.