Data yang berpindah lewat jaringan pada Chapter sebelumnya butuh kesepakatan format, dan format yang paling umum hari ini adalah JSON — teks berstruktur yang menjadi bahasa resmi API web, file konfigurasi modern, dan hampir semua pertukaran data antar service. Di sinilah C++ menunjukkan wataknya yang khas: standard library tidak menyediakan dukungan JSON sama sekali, sehingga realitas industri adalah library eksternal — yang di lingkungan course ini tidak terinstal, sehingga pola pemakaiannya kita tampilkan sebagai kode untuk Anda jalankan sendiri. Sebagai gantinya, kita tidak hanya membaca teori: kita bangun mini JSON writer dan mini JSON parser sendiri, uji keduanya sungguhan, dan dengan itu memahami persis pekerjaan apa yang dilakukan library tersebut di balik API-nya yang semudah j["nama"].
Format JSON dalam Satu Pandangan #
Dokumen JSON berisi nilai dari enam tipe saja: null, true/false, angka, string, array, dan object (pasangan kunci-nilai). Kuncinya selalu string, dan string mengikuti aturan escaping yang ketat: tanda kutip ditulis \", backslash \\, newline \n, dan karakter kontrol lain dalam bentuk \uXXXX. Dua aturan inilah — enam tipe dan escaping — yang membuat JSON bisa disisipkan aman di dalam JSON lain, dan dua aturan inilah juga yang menjadi pekerjaan utama writer dan parser.
nlohmann/json: Pola Pemakaian #
Library yang paling populer untuk C++ adalah nlohmann/json
— header tunggal yang bisa diunduh dan langsung disertakan, dirancang agar JSON terasa seperti container STL. Cara memasangnya sesederhana mengunduh json.hpp ke proyek (atau lewat package manager), lalu:
#include <nlohmann/json.hpp>
using json = nlohmann::json;
json pesanan; // bangun dokumen seperti container biasa
pesanan["nomor"] = "RSI-2026-0042";
pesanan["berat_kg"] = 1.5;
pesanan["item"] = {"kabel USB", "adaptor"};
pesanan["pelanggan"]["kota"] = "Bandung"; // nesting otomatis
std::string teks = pesanan.dump(2); // serialize, indentasi 2 spasi
json balikan = json::parse(teks); // sebaliknya: teks -> object
double berat = balikan["berat_kg"]; // konversi otomatis ke double
for (const auto& item : balikan["item"]) { // iterasi array
std::printf("item: %s\n", item.get<std::string>().c_str());
}Pola yang sama berlaku untuk membaca file konfigurasi: json::parse(ifstream) mengubah seluruh isi file menjadi object yang bisa diakses dengan operator[]. Konversi tipe dilakukan saat runtime — meminta double dari kunci yang berisi array melempar type_error, jadi struktur yang diharapkan perlu diverifikasi dengan contains() atau is_number() sebelum diakses.
RapidJSON: Untuk Performa Tinggi #
Ketika dokumen JSON-nya ratusan MB (log besar, dataset), nlohmann/json mulai terasa berat karena banyak menyalin string. RapidJSON dirancang untuk kasus itu: tanpa alokasi berlebih, zero-copy untuk string, dan dua gaya pemrosesan — DOM (muat semua seperti nlohmann) atau SAX (streaming: peristiwa StartObject, String, EndArray dipicu saat membaca, tanpa membangun pohon). API-nya lebih verbose dan memakai alokator eksplisit, sehingga pemakaiannya jauh lebih melelahkan; aturan praktisnya, mulai dari nlohmann/json dan pindah ke RapidJSON hanya setelah profil menunjukkan parsing JSON sebagai hambatan nyata.
Mini JSON Writer yang Teruji #
Sekarang kita bangun sendiri potongan kecil dari pekerjaan itu: writer yang meng-escape string dan menyusun object bersarang. Program ini kita kompilasi dan jalankan, dan outputnya kita validasi sebagai JSON sah:
// json-writer.cpp — penulis JSON mini buatan sendiri: escaping string,
// nesting object/array, angka, bool, dan null. Persis pekerjaan yang
// diotomatiskan library seperti nlohmann/json.
#include <cstdio>
#include <string>
// Bungkus string menjadi token JSON yang aman: tanda kutip, backslash,
// dan karakter kontrol di-escape sesuai spesifikasi JSON.
std::string escapeJson(const std::string& s) {
std::string hasil;
hasil += '"';
for (char c : s) {
switch (c) {
case '"': hasil += "\\\""; break;
case '\\': hasil += "\\\\"; break;
case '\n': hasil += "\\n"; break;
case '\t': hasil += "\\t"; break;
case '\r': hasil += "\\r"; break;
default:
if (static_cast<unsigned char>(c) < 0x20) {
char buf[8];
std::snprintf(buf, sizeof(buf), "\\u%04x",
static_cast<unsigned>(static_cast<unsigned char>(c)));
hasil += buf;
} else {
hasil += c;
}
}
}
hasil += '"';
return hasil;
}
std::string indentasi(int level) { return std::string(static_cast<size_t>(level) * 2, ' '); }
// Overload per tipe: satu nama fungsi, konversi otomatis mengikuti argumen.
void baris(std::string& k, int level, const std::string& kunci, const std::string& nilai) {
k += indentasi(level) + escapeJson(kunci) + ": " + escapeJson(nilai) + ",\n";
}
// Overload const char* wajib ada dan diletakkan SETELAH overload di atas:
// tanpa ini, literal string terjebak ke overload bool karena pointer-to-bool
// adalah standard conversion yang menang atas konversi ke std::string.
void baris(std::string& k, int level, const std::string& kunci, const char* nilai) {
baris(k, level, kunci, std::string(nilai));
}
void baris(std::string& k, int level, const std::string& kunci, int nilai) {
k += indentasi(level) + escapeJson(kunci) + ": " + std::to_string(nilai) + ",\n";
}
void baris(std::string& k, int level, const std::string& kunci, double nilai) {
char buf[32];
std::snprintf(buf, sizeof(buf), "%.10g", nilai);
k += indentasi(level) + escapeJson(kunci) + ": " + buf + ",\n";
}
void baris(std::string& k, int level, const std::string& kunci, bool nilai) {
k += indentasi(level) + escapeJson(kunci) + ": " + (nilai ? "true" : "false") + ",\n";
}
void baris(std::string& k, int level, const std::string& kunci, std::nullptr_t) {
k += indentasi(level) + escapeJson(kunci) + ": null,\n";
}
int main() {
std::string json = "{\n";
baris(json, 1, "nomor", "RSI-2026-0042");
baris(json, 1, "terkirim", true);
baris(json, 1, "berat_kg", 1.5);
baris(json, 1, "catatan", "Kirim ke \"Gudang B\",\nLantai 2");
std::string pelanggan = "{\n"; // nested object dibangun terpisah
baris(pelanggan, 2, "nama", "Dewi");
baris(pelanggan, 2, "kota", "Bandung");
pelanggan.replace(pelanggan.size() - 2, 2, "\n");
pelanggan += indentasi(1) + "}";
json += indentasi(1) + "\"pelanggan\": " + pelanggan + ",\n";
json += indentasi(1) + "\"item\": [" + escapeJson("kabel USB") + ", "
+ escapeJson("adaptor") + "],\n";
baris(json, 1, "kupon", nullptr);
json.replace(json.size() - 2, 2, "\n"); // buang koma terakhir, sisakan newline
json += "}";
std::printf("%s\n", json.c_str());
return 0;
}Output yang Dihasilkan:
{
"nomor": "RSI-2026-0042",
"terkirim": true,
"berat_kg": 1.5,
"catatan": "Kirim ke \"Gudang B\",\nLantai 2",
"pelanggan": {
"nama": "Dewi",
"kota": "Bandung"
},
"item": ["kabel USB", "adaptor"],
"kupon": null
}Bedah bagian pentingnya:
- Baris 9-32 (
escapeJson) — jantung keamanan writer: setiap karakter diperiksa, kutip dan backslash diberi prefiks\\, karakter kontrol di bawah0x20dirender\uXXXX, sisanya lolos apa adanya. Tanpa fungsi ini, string berisi"atau newline akan menghasilkan JSON yang tidak sah. - Baris 36-45 (overload
baris) — satu nama untuk semua tipe nilai; compiler memilih versiint,double,bool, ataunullptrmengikuti argumen. Inilah versi manual dari “konversi otomatis” yang dijanjikan library. - Baris 40-45 (overload
const char*) — gotcha yang kita temui saat menguji: tanpa overload ini, literal string seperti"RSI-2026-0042"terjebak ke overloadbool, karena konversi pointer-to-bool adalah standard conversion yang mengalahkan konversi user-defined kestd::string. Hasilnya JSON yang terlihat sah tetapi isinyatrue— bug senyap yang hanya ketahuan lewat pengujian. - Baris 66 (
baris(json, 1, "catatan", ...)) — nilai berisi kutip ganda dan newline asli; perhatikan di output keduanya tampil sebagai\"dan\n— dua byte, bukan karakter asli. - Baris 67-72 (nested object) — object dalam dibangun sebagai string terpisah lalu disisipkan; pendekatan yang sama berlaku untuk array di baris 73-74.
- Baris 76-77 (koma terakhir) — JSON melarang koma sebelum
}, sehingga writer harus memangkasnya secara eksplisit; detail kecil seperti inilah yang membuat library sekelas nlohmann/json jauh lebih sulit ditulis daripada terlihat.
Mini JSON Parser yang Teruji #
Arah sebaliknya — teks menjadi struktur data — dikerjakan parser. Versi mini kita memakai teknik recursive descent: satu fungsi per tipe nilai, saling memanggil mengikuti struktur bahasa JSON itu sendiri.
// json-parser.cpp — parser JSON manual minimal (recursive descent):
// mendukung null, bool, angka, string dengan escape, array, dan object.
// Ini versi sederhana dari pekerjaan yang dilakukan parser library JSON.
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
struct Nilai {
enum class Tipe { Nol, Boole, Angka, Teks, Larik, Objek };
Tipe tipe = Tipe::Nol;
bool boole = false;
double angka = 0.0;
std::string teks;
std::vector<Nilai> larik;
std::vector<std::pair<std::string, Nilai>> objek;
const Nilai& ambil(const std::string& kunci) const {
for (const auto& [k, v] : objek) {
if (k == kunci) return v;
}
throw std::runtime_error("kunci tidak ditemukan: " + kunci);
}
};
class Parser {
public:
explicit Parser(const std::string& s) : s_(s) {}
Nilai parse() {
Nilai v = nilai_();
lewatiSpasi();
if (i_ != s_.size()) throw std::runtime_error("sisa karakter tak terduga");
return v;
}
private:
const std::string& s_;
size_t i_ = 0;
void lewatiSpasi() {
while (i_ < s_.size() &&
std::isspace(static_cast<unsigned char>(s_[i_]))) ++i_;
}
char lihat() const {
if (i_ >= s_.size()) throw std::runtime_error("input habis tiba-tiba");
return s_[i_];
}
void harapkan(char c) {
if (lihat() != c) throw std::runtime_error(std::string("harapkan '") + c + "'");
++i_;
}
void konsumsi(const char* kata) {
for (const char* p = kata; *p; ++p) {
if (i_ >= s_.size() || s_[i_] != *p)
throw std::runtime_error(std::string("harapkan kata '") + kata + "'");
++i_;
}
}
Nilai nilai_() {
lewatiSpasi();
switch (lihat()) {
case '{': return objek_();
case '[': return larik_();
case '"': {
Nilai v; v.tipe = Nilai::Tipe::Teks; v.teks = teks_(); return v;
}
case 't': konsumsi("true");
{ Nilai v; v.tipe = Nilai::Tipe::Boole; v.boole = true; return v; }
case 'f': konsumsi("false");
{ Nilai v; v.tipe = Nilai::Tipe::Boole; v.boole = false; return v; }
case 'n': konsumsi("null"); return Nilai{}; // Tipe::Nol
default: return angka_();
}
}
Nilai angka_() {
char* akhir = nullptr;
double hasil = std::strtod(s_.c_str() + i_, &akhir);
if (akhir == s_.c_str() + i_) throw std::runtime_error("angka tidak valid");
i_ = static_cast<size_t>(akhir - s_.c_str());
Nilai v; v.tipe = Nilai::Tipe::Angka; v.angka = hasil;
return v;
}
std::string teks_() {
harapkan('"');
std::string hasil;
while (true) {
if (i_ >= s_.size()) throw std::runtime_error("string tak ditutup");
char c = s_[i_++];
if (c == '"') return hasil;
if (c == '\\') { // escape sequence
if (i_ >= s_.size()) throw std::runtime_error("escape terpotong");
char e = s_[i_++];
switch (e) {
case '"': hasil += '"'; break;
case '\\': hasil += '\\'; break;
case '/': hasil += '/'; break;
case 'n': hasil += '\n'; break;
case 't': hasil += '\t'; break;
case 'r': hasil += '\r'; break;
case 'u': { // \uXXXX: cukup untuk ASCII
if (i_ + 4 > s_.size()) throw std::runtime_error("\\u terpotong");
int kode = std::stoi(s_.substr(i_, 4), nullptr, 16);
i_ += 4;
hasil += (kode < 128) ? static_cast<char>(kode) : '?';
break;
}
default: throw std::runtime_error("escape tidak dikenal");
}
} else {
hasil += c;
}
}
}
Nilai larik_() {
harapkan('[');
Nilai v; v.tipe = Nilai::Tipe::Larik;
lewatiSpasi();
if (lihat() == ']') { ++i_; return v; }
while (true) {
v.larik.push_back(nilai_());
lewatiSpasi();
char c = lihat();
if (c == ',') { ++i_; continue; }
if (c == ']') { ++i_; return v; }
throw std::runtime_error("harapkan ',' atau ']'");
}
}
Nilai objek_() {
harapkan('{');
Nilai v; v.tipe = Nilai::Tipe::Objek;
lewatiSpasi();
if (lihat() == '}') { ++i_; return v; }
while (true) {
lewatiSpasi();
std::string kunci = teks_();
lewatiSpasi();
harapkan(':');
v.objek.emplace_back(std::move(kunci), nilai_());
lewatiSpasi();
char c = lihat();
if (c == ',') { ++i_; continue; }
if (c == '}') { ++i_; return v; }
throw std::runtime_error("harapkan ',' atau '}'");
}
}
};
int main() {
const std::string dokumen = R"json({
"produk": "Kopi Gayo",
"harga": 85000,
"stok": 42,
"aktif": true,
"tag": ["arabika", "sangrai gelap"],
"diskon": null
})json";
try {
Nilai akar = Parser(dokumen).parse();
std::printf("produk = %s\n", akar.ambil("produk").teks.c_str());
std::printf("harga = %.0f\n", akar.ambil("harga").angka);
std::printf("stok = %.0f\n", akar.ambil("stok").angka);
std::printf("aktif = %s\n", akar.ambil("aktif").boole ? "true" : "false");
std::printf("tag[0] = %s\n", akar.ambil("tag").larik[0].teks.c_str());
std::printf("tag[1] = %s\n", akar.ambil("tag").larik[1].teks.c_str());
std::printf("jumlah tag = %zu\n", akar.ambil("tag").larik.size());
std::printf("diskon adalah null? %s\n",
akar.ambil("diskon").tipe == Nilai::Tipe::Nol ? "ya" : "tidak");
} catch (const std::runtime_error& e) {
std::printf("parse gagal: %s\n", e.what());
return 1;
}
return 0;
}Output yang Dihasilkan:
produk = Kopi Gayo
harga = 85000
stok = 42
aktif = true
tag[0] = arabika
tag[1] = sangrai gelap
jumlah tag = 2
diskon adalah null? yaBedah bagian pentingnya:
- Baris 12-27 (
struct Nilai) — satu struct mewakili semua kemungkinan nilai: enumTipemenentukan field mana yang sah, danambil()mencari kunci di object sambil melempar exception bila tidak ada. - Baris 64-79 (
nilai_()) — dispatcher inti: melihat satu karakter berikutnya dan menyerahkan pekerjaan ke parser yang tepat ({ke object,[ke array,"ke string, dst.). - Baris 90-120 (
teks_()) — membaca string sampai kutip penutup, sambil membalik escape:\"menjadi",\nmenjadi newline asli, dan\uXXXXditerjemahkan untuk rentang ASCII — kebalikan persis dariescapeJsondi writer. - Baris 81-88 (
angka_()) —strtodmem-parsing angka sekaligus memberi tahu sampai karakter mana ia berjalan; posisi kursori_dimutakhirkan dari hasilnya. - Baris 137-154 (
objek_()) — loop kunci-nilai yang memanggilnilai_()kembali; rekursi inilah yang membuat nesting tanpa batas bisa diproses dengan kode yang pendek. - Baris 168 (
Parser(dokumen).parse()) — jika input tidak sah, exception diteruskan kecatchdimaindan program keluar dengan kode 1; parser yang baik melapor di mana kesalahan terjadi, meski versi mini ini masih menyebut jenis kesalahannya saja.
| Pilihan | Cocok untuk |
|---|---|
| nlohmann/json | Kemudahan dan keterbacaan; dokumen normal; default yang tepat |
| RapidJSON | Dokumen besar, jalur panas parsing, kendali alokasi |
| Tulis sendiri | Belajar mekanisme; format internal sederhana yang terkendali |
Dengan writer dan parser yang sudah Anda pahami sampai ke byte-nya, format teks seperti JSON tidak lagi menjadi teka-teki — hanya detail API. Dari data terstruktur, kita naik ke data yang berbeda sifatnya: gambar, matriks piksel, dan dunia computer vision di Chapter berikutnya.
Referensi: JSON.org — spesifikasi ringkas , nlohmann/json di GitHub .