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:

cpp Pola nlohmann/json (ilustrasi)
#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:

cpp
// 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:

text
{
  "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:

  1. Baris 9-32 (escapeJson) — jantung keamanan writer: setiap karakter diperiksa, kutip dan backslash diberi prefiks \\, karakter kontrol di bawah 0x20 dirender \uXXXX, sisanya lolos apa adanya. Tanpa fungsi ini, string berisi " atau newline akan menghasilkan JSON yang tidak sah.
  2. Baris 36-45 (overload baris) — satu nama untuk semua tipe nilai; compiler memilih versi int, double, bool, atau nullptr mengikuti argumen. Inilah versi manual dari “konversi otomatis” yang dijanjikan library.
  3. Baris 40-45 (overload const char*) — gotcha yang kita temui saat menguji: tanpa overload ini, literal string seperti "RSI-2026-0042" terjebak ke overload bool, karena konversi pointer-to-bool adalah standard conversion yang mengalahkan konversi user-defined ke std::string. Hasilnya JSON yang terlihat sah tetapi isinya true — bug senyap yang hanya ketahuan lewat pengujian.
  4. 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.
  5. Baris 67-72 (nested object) — object dalam dibangun sebagai string terpisah lalu disisipkan; pendekatan yang sama berlaku untuk array di baris 73-74.
  6. 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.

cpp
// 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:

text
produk  = Kopi Gayo
harga   = 85000
stok    = 42
aktif   = true
tag[0]  = arabika
tag[1]  = sangrai gelap
jumlah tag = 2
diskon adalah null? ya

Bedah bagian pentingnya:

  1. Baris 12-27 (struct Nilai) — satu struct mewakili semua kemungkinan nilai: enum Tipe menentukan field mana yang sah, dan ambil() mencari kunci di object sambil melempar exception bila tidak ada.
  2. Baris 64-79 (nilai_()) — dispatcher inti: melihat satu karakter berikutnya dan menyerahkan pekerjaan ke parser yang tepat ({ ke object, [ ke array, " ke string, dst.).
  3. Baris 90-120 (teks_()) — membaca string sampai kutip penutup, sambil membalik escape: \" menjadi ", \n menjadi newline asli, dan \uXXXX diterjemahkan untuk rentang ASCII — kebalikan persis dari escapeJson di writer.
  4. Baris 81-88 (angka_()) — strtod mem-parsing angka sekaligus memberi tahu sampai karakter mana ia berjalan; posisi kursor i_ dimutakhirkan dari hasilnya.
  5. Baris 137-154 (objek_()) — loop kunci-nilai yang memanggil nilai_() kembali; rekursi inilah yang membuat nesting tanpa batas bisa diproses dengan kode yang pendek.
  6. Baris 168 (Parser(dokumen).parse()) — jika input tidak sah, exception diteruskan ke catch di main dan program keluar dengan kode 1; parser yang baik melapor di mana kesalahan terjadi, meski versi mini ini masih menyebut jenis kesalahannya saja.
PilihanCocok untuk
nlohmann/jsonKemudahan dan keterbacaan; dokumen normal; default yang tepat
RapidJSONDokumen besar, jalur panas parsing, kendali alokasi
Tulis sendiriBelajar 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 .