JSON #
JSON (JavaScript Object Notation) adalah format pertukaran data yang mendominasi ekosistem web modern — hampir semua REST API, webhook, dan layanan mikro berkomunikasi via JSON. PHP menyediakan dua fungsi utama, json_encode() dan json_decode(), yang terlihat sederhana tapi menyimpan banyak detail penting: penanganan error yang sering diabaikan, flag yang mengubah output secara signifikan, perbedaan antara decode ke object vs array, encoding karakter Unicode, dan cara kerja JsonSerializable yang memungkinkan kontrol penuh atas representasi JSON sebuah objek. Artikel ini membahas semua aspek JSON di PHP secara mendalam, termasuk streaming untuk dataset besar dan pola-pola yang membuat kode REST API lebih andal.
json_encode() — PHP ke JSON
#
json_encode() mengubah nilai PHP menjadi string JSON. Ia mengembalikan false jika encoding gagal — dan ini lebih sering terjadi dari yang disangka:
<?php
// Tipe data dasar
echo json_encode(42); // 42
echo json_encode(3.14); // 3.14
echo json_encode(true); // true
echo json_encode(false); // false
echo json_encode(null); // null
echo json_encode("Halo"); // "Halo"
// Array
echo json_encode([1, 2, 3]); // [1,2,3]
echo json_encode(['a', 'b']); // ["a","b"]
// Array asosiatif → JSON object
echo json_encode(['nama' => 'Budi', 'umur' => 28]);
// {"nama":"Budi","umur":28}
// Multidimensi
$data = [
'user' => ['id' => 1, 'nama' => 'Budi'],
'orders' => [
['id' => 101, 'total' => 150000],
['id' => 102, 'total' => 75000],
],
];
echo json_encode($data);
// {"user":{"id":1,"nama":"Budi"},"orders":[{"id":101,"total":150000},{"id":102,"total":75000}]}
Flag json_encode() yang Penting
#
<?php
$data = [
'nama' => 'Budi Santoso',
'kota' => 'Jakarta',
'url' => 'https://example.com/produk?id=1&cat=2',
'html' => '<b>Tebal</b>',
'harga' => 150000.5,
];
// JSON_PRETTY_PRINT — format dengan indentasi (untuk debug/API yang dibaca manusia)
echo json_encode($data, JSON_PRETTY_PRINT);
/*
{
"nama": "Budi Santoso",
"kota": "Jakarta",
"url": "https:\/\/example.com\/produk?id=1&cat=2",
"html": "\u003Cb\u003ETebal\u003C\/b\u003E",
"harga": 150000.5
}
*/
// JSON_UNESCAPED_UNICODE — karakter Unicode tidak di-escape
$unicode = ['emoji' => '😀', 'arab' => 'مرحبا'];
echo json_encode($unicode);
// {"emoji":"\ud83d\ude00","arab":"\u0645\u0631\u062d\u0628\u0627"}
echo json_encode($unicode, JSON_UNESCAPED_UNICODE);
// {"emoji":"😀","arab":"مرحبا"}
// JSON_UNESCAPED_SLASHES — slash tidak di-escape (berguna untuk URL)
echo json_encode(['url' => 'https://example.com']);
// {"url":"https:\/\/example.com"}
echo json_encode(['url' => 'https://example.com'], JSON_UNESCAPED_SLASHES);
// {"url":"https://example.com"}
// JSON_THROW_ON_ERROR — lempar exception alih-alih return false (PHP 7.3+)
// Ini adalah cara yang direkomendasikan — selalu gunakan ini
echo json_encode($data, JSON_THROW_ON_ERROR);
// Gabung beberapa flag dengan operator bitwise |
$flags = JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
echo json_encode($data, $flags);
// JSON_FORCE_OBJECT — paksa array numerik menjadi object
echo json_encode([1, 2, 3]);
// [1,2,3]
echo json_encode([1, 2, 3], JSON_FORCE_OBJECT);
// {"0":1,"1":2,"2":3}
// JSON_NUMERIC_CHECK — konversi string numerik ke angka
echo json_encode(['harga' => '150000', 'qty' => '3']);
// {"harga":"150000","qty":"3"}
echo json_encode(['harga' => '150000', 'qty' => '3'], JSON_NUMERIC_CHECK);
// {"harga":150000,"qty":3}
// PERINGATAN: ini bisa mengubah ID string seperti "007" menjadi 7
// JSON_PRESERVE_ZERO_FRACTION — pertahankan .0 untuk float yang merupakan bilangan bulat
echo json_encode(['nilai' => 1.0]);
// {"nilai":1} ← PHP 7 default: hilangkan .0
echo json_encode(['nilai' => 1.0], JSON_PRESERVE_ZERO_FRACTION);
// {"nilai":1.0} ← lebih akurat untuk API yang peduli tipe
Penanganan Error json_encode()
#
json_encode() gagal secara diam-diam jika tidak menggunakan JSON_THROW_ON_ERROR:
<?php
// ANTI-PATTERN: abaikan kemungkinan kegagalan
$json = json_encode($data);
echo $json; // mungkin false! — tapi tidak ada error
// Sumber kegagalan umum:
// 1. String yang mengandung encoding UTF-8 yang tidak valid
$rusak = "Teks dengan byte \xFF yang rusak";
var_dump(json_encode($rusak)); // bool(false)
echo json_last_error(); // JSON_ERROR_UTF8 (5)
echo json_last_error_msg(); // "Malformed UTF-8 characters, possibly incorrectly encoded"
// 2. Nilai float yang tidak valid (INF, NAN)
var_dump(json_encode(INF)); // bool(false)
var_dump(json_encode(NAN)); // bool(false)
// 3. Kedalaman rekursi melebihi batas (default 512)
// Array atau objek yang sangat dalam (> 512 level)
// BENAR: gunakan JSON_THROW_ON_ERROR
function encodeAman(mixed $data, int $flags = 0): string
{
try {
return json_encode($data, $flags | JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
throw new \RuntimeException(
"Gagal encode JSON: " . $e->getMessage(),
previous: $e
);
}
}
// Atau tangkap langsung
try {
$json = json_encode($data, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
} catch (\JsonException $e) {
// Tangani error
error_log("JSON encode gagal: " . $e->getMessage());
throw $e;
}
// Sanitasi string bermasalah sebelum encode
function sanitasiUtf8(string $string): string
{
// Hapus atau ganti karakter yang tidak valid UTF-8
return mb_convert_encoding($string, 'UTF-8', 'UTF-8');
}
json_decode() — JSON ke PHP
#
<?php
$json = '{"nama":"Budi","umur":28,"aktif":true,"skor":null}';
// Default: decode ke stdClass object
$obj = json_decode($json);
echo $obj->nama; // Budi
echo $obj->umur; // 28
var_dump($obj->aktif); // bool(true)
var_dump($obj->skor); // NULL
// Argumen kedua true: decode ke array asosiatif
$arr = json_decode($json, associative: true);
echo $arr['nama']; // Budi
echo $arr['umur']; // 28
// Memilih antara object dan array
// → Gunakan array jika hanya butuh akses data
// → Gunakan object jika butuh type hint atau method
Decode ke Object vs Array — Kapan Mana #
<?php
// Decode ke array — lebih umum dan langsung
$data = json_decode($json, associative: true);
if (!is_array($data)) {
throw new \JsonException("JSON tidak valid atau bukan object/array");
}
echo $data['user']['nama'];
foreach ($data['items'] as $item) {
echo $item['nama'] . ": Rp" . $item['harga'] . "\n";
}
// Decode ke stdClass — berguna saat ingin akses seperti object
$obj = json_decode($json);
echo $obj->user->nama;
// PERHATIAN: akses property yang tidak ada tidak memunculkan error di PHP 8+
// tapi menghasilkan null — sama seperti array dengan key yang tidak ada
echo $obj->propertiTidakAda ?? 'default'; // null, bukan error
// Decode ke kelas tertentu — manual, tidak ada cara native
// Harus mapping manual
class UserDTO
{
public function __construct(
public readonly int $id,
public readonly string $nama,
public readonly string $email,
) {}
public static function fromJson(string $json): static
{
$data = json_decode($json, associative: true, flags: JSON_THROW_ON_ERROR);
return new static(
id: $data['id'],
nama: $data['nama'],
email: $data['email'],
);
}
public static function fromArray(array $data): static
{
return new static(
id: (int) ($data['id'] ?? throw new \InvalidArgumentException("id diperlukan")),
nama: (string) ($data['nama'] ?? throw new \InvalidArgumentException("nama diperlukan")),
email: (string) ($data['email'] ?? throw new \InvalidArgumentException("email diperlukan")),
);
}
}
$user = UserDTO::fromJson('{"id":1,"nama":"Budi","email":"[email protected]"}');
echo $user->nama; // Budi
Penanganan Error json_decode()
#
<?php
// ANTI-PATTERN: tidak validasi hasil decode
$data = json_decode($input, associative: true);
echo $data['nama']; // fatal error jika $data null (JSON tidak valid)
// BENAR: gunakan JSON_THROW_ON_ERROR dan validasi tipe
function decodeAman(string $json): array
{
try {
$data = json_decode($json, associative: true, flags: JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
throw new \InvalidArgumentException("JSON tidak valid: " . $e->getMessage(), previous: $e);
}
if (!is_array($data)) {
throw new \InvalidArgumentException("JSON harus berupa object atau array, bukan: " . gettype($data));
}
return $data;
}
// Validasi struktur JSON yang diterima dari API eksternal
function validasiStruktur(array $data, array $fieldWajib): void
{
foreach ($fieldWajib as $field) {
if (!array_key_exists($field, $data)) {
throw new \RuntimeException("Field '$field' tidak ada dalam respons");
}
}
}
$respons = decodeAman($jsonDariApi);
validasiStruktur($respons, ['id', 'status', 'data']);
JsonSerializable Interface
#
Untuk mengontrol bagaimana sebuah objek di-encode ke JSON, implementasikan interface JsonSerializable:
<?php
class Order implements \JsonSerializable
{
public function __construct(
private int $id,
private string $status,
private float $total,
private array $items,
private \DateTimeImmutable $dibuat,
private ?string $catatan = null,
) {}
// Method ini dipanggil otomatis oleh json_encode()
public function jsonSerialize(): mixed
{
return [
'id' => $this->id,
'status' => $this->status,
'total' => $this->total,
'items' => $this->items,
'dibuat' => $this->dibuat->format(\DateTimeInterface::ISO8601),
// Sembunyikan catatan internal jika null
...($this->catatan !== null ? ['catatan' => $this->catatan] : []),
];
}
// Getter untuk akses internal
public function getId(): int { return $this->id; }
public function getTotal(): float { return $this->total; }
}
$order = new Order(
id: 42,
status: 'selesai',
total: 150_000,
items: [['nama' => 'Laptop', 'qty' => 1]],
dibuat: new \DateTimeImmutable('2024-03-15 14:00:00'),
);
echo json_encode($order, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
/*
{
"id": 42,
"status": "selesai",
"total": 150000,
"items": [{"nama":"Laptop","qty":1}],
"dibuat": "2024-03-15T14:00:00+0000"
}
*/
// Catatan: bahkan jika ada property sensitif seperti $password, ia tidak akan muncul
// dalam JSON karena jsonSerialize() mengontrol apa yang di-ekspos
Hierarki JsonSerializable #
<?php
// Objek bersarang yang masing-masing implement JsonSerializable
class Money implements \JsonSerializable
{
public function __construct(
private int $jumlah, // dalam sen
private string $mata, // 'IDR', 'USD', dll.
) {}
public function jsonSerialize(): mixed
{
return [
'jumlah' => $this->jumlah,
'mata_uang' => $this->mata,
'format' => $this->format(),
];
}
public function format(): string
{
return match($this->mata) {
'IDR' => 'Rp ' . number_format($this->jumlah / 100, 0, ',', '.'),
'USD' => '$' . number_format($this->jumlah / 100, 2),
default => $this->mata . ' ' . ($this->jumlah / 100),
};
}
}
class Produk implements \JsonSerializable
{
public function __construct(
private int $id,
private string $nama,
private Money $harga, // berisi JsonSerializable lain
) {}
public function jsonSerialize(): mixed
{
return [
'id' => $this->id,
'nama' => $this->nama,
'harga' => $this->harga, // json_encode otomatis panggil jsonSerialize() pada Money
];
}
}
$produk = new Produk(1, 'Laptop', new Money(15_000_000_00, 'IDR')); // 15 juta dalam sen
echo json_encode($produk, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
/*
{
"id": 1,
"nama": "Laptop",
"harga": {
"jumlah": 1500000000,
"mata_uang": "IDR",
"format": "Rp 15.000.000"
}
}
*/
Pola REST API Response yang Konsisten #
Salah satu penggunaan JSON paling penting di PHP adalah membangun REST API. Konsistensi format response sangat penting untuk konsumen API:
<?php
class ApiResponse
{
private function __construct(
private readonly bool $sukses,
private readonly mixed $data,
private readonly ?string $pesan = null,
private readonly array $errors = [],
private readonly array $meta = [],
) {}
public static function ok(mixed $data, string $pesan = null, array $meta = []): static
{
return new static(true, $data, $pesan, [], $meta);
}
public static function gagal(string $pesan, array $errors = [], int $kode = 400): static
{
http_response_code($kode);
return new static(false, null, $pesan, $errors);
}
public static function tidakDitemukan(string $resource): static
{
return static::gagal("$resource tidak ditemukan", kode: 404);
}
public static function tidakDiizinkan(): static
{
return static::gagal("Akses tidak diizinkan", kode: 403);
}
public function kirim(): void
{
header('Content-Type: application/json; charset=utf-8');
$payload = ['sukses' => $this->sukses];
if ($this->pesan !== null) {
$payload['pesan'] = $this->pesan;
}
if ($this->sukses) {
$payload['data'] = $this->data;
} else {
$payload['errors'] = $this->errors;
}
if (!empty($this->meta)) {
$payload['meta'] = $this->meta;
}
echo json_encode(
$payload,
JSON_THROW_ON_ERROR
| JSON_UNESCAPED_UNICODE
| JSON_UNESCAPED_SLASHES
);
}
}
// Penggunaan di controller
function getUser(int $id): void
{
$user = findUser($id);
if ($user === null) {
ApiResponse::tidakDitemukan('User')->kirim();
return;
}
ApiResponse::ok($user, meta: [
'cached_at' => date(\DateTimeInterface::ISO8601),
])->kirim();
}
function createUser(array $input): void
{
$errors = validasiInput($input);
if (!empty($errors)) {
ApiResponse::gagal("Validasi gagal", $errors, 422)->kirim();
return;
}
$user = simpanUser($input);
http_response_code(201);
ApiResponse::ok($user, "User berhasil dibuat")->kirim();
}
Streaming JSON untuk Dataset Besar #
Memuat seluruh dataset besar ke memori sebelum encode bisa menyebabkan out-of-memory. Untuk kasus ini, streaming JSON lebih tepat:
<?php
// ANTI-PATTERN: encode semua sekaligus untuk dataset besar
$semuaProduk = $db->query("SELECT * FROM produk")->fetchAll(); // 100.000 baris!
echo json_encode($semuaProduk); // memori habis!
// BENAR: stream JSON secara manual
function streamJsonArray(iterable $items, callable $transform = null): void
{
header('Content-Type: application/json; charset=utf-8');
echo '[';
$pertama = true;
foreach ($items as $item) {
if (!$pertama) {
echo ',';
}
$pertama = false;
$nilai = $transform !== null ? $transform($item) : $item;
echo json_encode($nilai, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
// Flush output buffer secara berkala agar tidak menumpuk di buffer
if (ob_get_level() > 0) {
ob_flush();
}
flush();
}
echo ']';
}
// Gunakan dengan cursor/generator dari database
$stmt = $pdo->query("SELECT id, nama, harga FROM produk ORDER BY id");
$stmt->setFetchMode(\PDO::FETCH_ASSOC);
streamJsonArray($stmt, function(array $row): array {
return [
'id' => (int) $row['id'],
'nama' => $row['nama'],
'harga' => (float) $row['harga'],
];
});
// Hanya satu baris di memori pada satu waktu — tidak peduli berapa juta baris
Validasi JSON Schema #
Untuk memvalidasi struktur JSON yang diterima dari API eksternal atau user input:
<?php
// Validasi manual — untuk schema sederhana
function validasiOrderJson(array $data): array
{
$errors = [];
// Cek field wajib
foreach (['user_id', 'items', 'alamat'] as $field) {
if (!array_key_exists($field, $data)) {
$errors[$field] = "Field '$field' wajib ada";
}
}
if (isset($data['items'])) {
if (!is_array($data['items']) || empty($data['items'])) {
$errors['items'] = "Items harus berupa array yang tidak kosong";
} else {
foreach ($data['items'] as $i => $item) {
if (!isset($item['produk_id']) || !is_int($item['produk_id'])) {
$errors["items.$i.produk_id"] = "produk_id harus berupa integer";
}
if (!isset($item['qty']) || $item['qty'] < 1) {
$errors["items.$i.qty"] = "qty harus minimal 1";
}
}
}
}
if (isset($data['user_id']) && !is_int($data['user_id'])) {
$errors['user_id'] = "user_id harus berupa integer";
}
return $errors;
}
// Untuk validasi schema yang kompleks, gunakan library:
// composer require justinrainbow/json-schema
use JsonSchema\Validator;
use JsonSchema\Constraints\Constraint;
$schema = json_decode(file_get_contents('schema/order.json'));
$data = json_decode($inputJson);
$validator = new Validator();
$validator->validate($data, $schema, Constraint::CHECK_MODE_APPLY_DEFAULTS);
if (!$validator->isValid()) {
$errors = [];
foreach ($validator->getErrors() as $error) {
$errors[$error['property']] = $error['message'];
}
// return validation errors
}
Transformasi Data JSON #
<?php
// Ubah format tanggal dalam response API
function normalisasiTanggal(array $data): array
{
$tanggalFields = ['created_at', 'updated_at', 'deleted_at', 'tanggal_lahir'];
foreach ($tanggalFields as $field) {
if (isset($data[$field]) && is_string($data[$field])) {
try {
$dt = new \DateTimeImmutable($data[$field]);
$data[$field] = $dt->format('Y-m-d H:i:s');
} catch (\Exception $e) {
// biarkan nilai asli jika tidak bisa diparse
}
}
}
return $data;
}
// Hapus field sensitif sebelum encode ke JSON
function sanitasiUntukPublik(array $user): array
{
$fieldsPrivate = ['password', 'password_hash', 'token', 'secret', 'pin'];
return array_diff_key($user, array_flip($fieldsPrivate));
}
// Transform snake_case ke camelCase untuk API JavaScript
function snakeToCamelKeys(array $data): array
{
$hasil = [];
foreach ($data as $kunci => $nilai) {
$kunciCamel = lcfirst(str_replace('_', '', ucwords($kunci, '_')));
$hasil[$kunciCamel] = is_array($nilai) ? snakeToCamelKeys($nilai) : $nilai;
}
return $hasil;
}
$dataDb = ['user_id' => 1, 'nama_lengkap' => 'Budi', 'created_at' => '2024-01-01'];
$dataApi = snakeToCamelKeys($dataDb);
echo json_encode($dataApi);
// {"userId":1,"namaLengkap":"Budi","createdAt":"2024-01-01"}
Anti-Pattern JSON yang Sering Ditemui #
<?php
// ✗ Anti-pattern 1: tidak handle error encode
$json = json_encode($data); // return false jika gagal!
echo $json; // output "false" ke client — invalid JSON
// ✓ Selalu gunakan JSON_THROW_ON_ERROR
$json = json_encode($data, JSON_THROW_ON_ERROR);
// ✗ Anti-pattern 2: decode lalu encode ulang untuk "validasi"
$valid = json_encode(json_decode($input)); // kehilangan tipe data (int → float, dll.)
// ✓ Validasi secara eksplisit
try {
$data = json_decode($input, true, flags: JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
throw new \InvalidArgumentException("Input bukan JSON valid");
}
// ✗ Anti-pattern 3: simpan JSON langsung ke database lalu decode berulang
// Setiap akses harus decode — mahal untuk data yang sering diakses
$db->query("UPDATE users SET config = ?", [json_encode($config)]);
$row = $db->query("SELECT config FROM users WHERE id = 1")->fetch();
$config = json_decode($row['config'], true); // decode setiap akses
// ✓ Decode sekali, simpan di variabel/cache
$config = json_decode($row['config'], true);
// Akses $config['theme'] bukan json_decode(..., true)['theme']
// ✗ Anti-pattern 4: gunakan json_decode untuk cek apakah string adalah JSON valid
if (json_decode($input) !== null) { /* valid? */ }
// json_decode("null") juga mengembalikan null — false negative!
// ✓ Cara yang benar
function isValidJson(string $string): bool
{
try {
json_decode($string, flags: JSON_THROW_ON_ERROR);
return true;
} catch (\JsonException) {
return false;
}
}
// ✗ Anti-pattern 5: encode object yang punya property sensitif tanpa filter
class User {
public int $id;
public string $nama;
public string $passwordHash; // BAHAYA jika ter-encode!
}
$user = new User();
echo json_encode($user); // {"id":1,"nama":"Budi","passwordHash":"$2y$..."}
// ✓ Implementasikan JsonSerializable untuk kontrol eksplisit
class User implements \JsonSerializable {
public function jsonSerialize(): mixed {
return ['id' => $this->id, 'nama' => $this->nama];
// passwordHash tidak disertakan
}
}
Ringkasan #
- Selalu gunakan
JSON_THROW_ON_ERROR— tanpanya,json_encode()danjson_decode()mengembalikanfalse/nullsecara diam-diam saat gagal.JSON_THROW_ON_ERRORmengubahnya menjadi\JsonExceptionyang bisa ditangkap.- Flag penting
json_encode():JSON_UNESCAPED_UNICODE(karakter Unicode tidak di-escape),JSON_UNESCAPED_SLASHES(URL-friendly),JSON_PRETTY_PRINT(untuk debugging),JSON_PRESERVE_ZERO_FRACTION(pertahankan.0pada float).json_decode($json, true)menghasilkan array asosiatif; tanpa argumen kedua menghasilkanstdClass. Gunakan array untuk sebagian besar kasus — lebih mudah dimanipulasi.JsonSerializableinterface memberikan kontrol penuh atas representasi JSON sebuah objek — gunakan untuk menyembunyikan field sensitif, mengubah format, atau mengompresi data sebelum encode.- Validasi setelah decode — jangan asumsikan JSON dari luar selalu valid atau punya struktur yang diharapkan. Periksa
is_array(), keberadaan key, dan tipe nilai.- Streaming JSON untuk dataset besar — emit
[, encode satu item pada satu waktu dengan pembatas,, tutup dengan]. HindarifetchAll()+json_encode()untuk ratusan ribu baris.- Sanitasi sebelum encode — hapus field sensitif (
password,token) sebelum JSON dikirim ke client.array_diff_key()berguna untuk ini.json_encode()otomatis memanggiljsonSerialize()pada objek yang mengimplementasikanJsonSerializable, termasuk objek bersarang — tidak perlu encode manual.