JSON

JSON #

JSON adalah format pertukaran data yang hampir universal di dunia web modern — hampir semua REST API, file konfigurasi, dan protokol komunikasi menggunakannya. Di Rust, semua kebutuhan JSON ditangani oleh dua crate yang bekerja bersama: serde sebagai framework serialisasi/deserialisasi generik, dan serde_json sebagai implementasinya untuk format JSON. Kombinasi ini sangat ergonomis: cukup tambahkan #[derive(Serialize, Deserialize)] ke struct, dan seluruh mekanisme konversi antara struct Rust dan JSON bekerja otomatis — tanpa menulis satu baris boilerplate. Artikel ini membahas dari penggunaan paling dasar hingga pola lanjutan seperti JSON dinamis, streaming, dan transformasi field.

Instalasi #

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"

Fitur "derive" pada serde mengaktifkan proc-macro #[derive(Serialize, Deserialize)] — tanpanya kamu harus mengimplementasikan trait tersebut secara manual.


Serialize dan Deserialize Dasar #

Struct ke JSON (Serialize) #

use serde::Serialize;
use serde_json;

#[derive(Serialize, Debug)]
struct Pengguna {
    id: u64,
    nama: String,
    email: String,
    aktif: bool,
    skor: f64,
}

fn main() {
    let pengguna = Pengguna {
        id: 1001,
        nama: String::from("Budi Santoso"),
        email: String::from("[email protected]"),
        aktif: true,
        skor: 95.5,
    };

    // Serialize ke JSON string yang compact
    let json_str = serde_json::to_string(&pengguna).unwrap();
    println!("{}", json_str);
    // {"id":1001,"nama":"Budi Santoso","email":"[email protected]","aktif":true,"skor":95.5}

    // Serialize ke JSON string yang pretty-printed (dengan indentasi)
    let json_cantik = serde_json::to_string_pretty(&pengguna).unwrap();
    println!("{}", json_cantik);

    // Serialize ke Vec<u8> (bytes) — berguna untuk mengirim via jaringan
    let json_bytes = serde_json::to_vec(&pengguna).unwrap();
    println!("Ukuran: {} byte", json_bytes.len());
}

JSON ke Struct (Deserialize) #

use serde::Deserialize;
use serde_json;

#[derive(Deserialize, Debug)]
struct Produk {
    id: u64,
    nama: String,
    harga: f64,
    stok: u32,
    kategori: String,
}

fn main() {
    let json_str = r#"{
        "id": 501,
        "nama": "Laptop Gaming",
        "harga": 18500000.0,
        "stok": 12,
        "kategori": "Elektronik"
    }"#;

    // Deserialize dari &str
    let produk: Produk = serde_json::from_str(json_str).unwrap();
    println!("{:?}", produk);

    // Deserialize dari &[u8] (bytes)
    let json_bytes = json_str.as_bytes();
    let produk2: Produk = serde_json::from_slice(json_bytes).unwrap();
    println!("Harga: Rp{:.0}", produk2.harga);

    // Deserialize dari io::Read (misalnya file)
    let file = std::fs::File::open("produk.json");
    // let produk3: Produk = serde_json::from_reader(file.unwrap()).unwrap();

    // Error handling yang benar — jangan unwrap di produksi
    match serde_json::from_str::<Produk>(r#"{"id": "bukan angka"}"#) {
        Ok(p) => println!("{:?}", p),
        Err(e) => println!("Parse error: {} di baris {}, kolom {}", e, e.line(), e.column()),
    }
}

Struct dengan Kedua Trait Sekaligus #

use serde::{Deserialize, Serialize};
use serde_json;

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
struct Pesanan {
    id: u64,
    produk: String,
    jumlah: u32,
    total: f64,
    status: String,
}

fn main() {
    let pesanan = Pesanan {
        id: 9001,
        produk: String::from("Kopi Arabika"),
        jumlah: 3,
        total: 150_000.0,
        status: String::from("diproses"),
    };

    // Roundtrip: struct → JSON → struct
    let json = serde_json::to_string(&pesanan).unwrap();
    let kembali: Pesanan = serde_json::from_str(&json).unwrap();

    assert_eq!(pesanan, kembali);
    println!("Roundtrip berhasil: {:?}", kembali);
}

Anotasi #[serde(...)] — Kontrol Format JSON #

Serde menyediakan anotasi yang kaya untuk mengontrol bagaimana field di-serialize dan di-deserialize tanpa mengubah nama field Rust.

rename — Nama Field Berbeda di JSON #

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
struct ResponAPI {
    // Nama Rust: id_pengguna, nama JSON: "userId"
    #[serde(rename = "userId")]
    id_pengguna: u64,

    #[serde(rename = "fullName")]
    nama_lengkap: String,

    #[serde(rename = "isActive")]
    aktif: bool,

    // Konvensi camelCase untuk seluruh struct bisa dilakukan di level struct
    #[serde(rename = "createdAt")]
    dibuat_pada: String,
}

// Cara lebih efisien: rename_all di level struct
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]  // semua field otomatis di-rename ke camelCase
struct RespponsCamel {
    user_id: u64,          // → "userId"
    full_name: String,     // → "fullName"
    is_active: bool,       // → "isActive"
    created_at: String,    // → "createdAt"
}

fn main() {
    let resp = RespponsCamel {
        user_id: 1,
        full_name: String::from("Sari Dewi"),
        is_active: true,
        created_at: String::from("2024-08-24"),
    };

    let json = serde_json::to_string_pretty(&resp).unwrap();
    println!("{}", json);
    // {
    //   "userId": 1,
    //   "fullName": "Sari Dewi",
    //   "isActive": true,
    //   "createdAt": "2024-08-24"
    // }
}

skip, default, dan skip_serializing_if #

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
struct Konfigurasi {
    host: String,
    port: u16,

    // Tidak disertakan saat serialisasi (tidak muncul di JSON output)
    #[serde(skip_serializing)]
    password: String,

    // Tidak disertakan sama sekali (diabaikan saat parse juga)
    #[serde(skip)]
    cache_internal: Option<String>,

    // Tidak disertakan di JSON jika nilainya None
    #[serde(skip_serializing_if = "Option::is_none")]
    komentar: Option<String>,

    // Gunakan nilai default jika field tidak ada di JSON
    #[serde(default)]
    debug: bool,  // default false jika tidak ada di JSON

    #[serde(default = "nilai_default_timeout")]
    timeout_detik: u32,
}

fn nilai_default_timeout() -> u32 {
    30
}

fn main() {
    let config = Konfigurasi {
        host: String::from("localhost"),
        port: 5432,
        password: String::from("rahasia"),
        cache_internal: Some(String::from("internal")),
        komentar: None,  // akan di-skip di JSON output
        debug: false,
        timeout_detik: 30,
    };

    let json = serde_json::to_string_pretty(&config).unwrap();
    println!("{}", json);
    // password tidak muncul, cache_internal tidak muncul, komentar tidak muncul

    // Deserialize: field yang tidak ada menggunakan default
    let json_minimal = r#"{"host": "db.contoh.com", "port": 5432, "password": ""}"#;
    let config2: Konfigurasi = serde_json::from_str(json_minimal).unwrap();
    println!("Timeout default: {}s", config2.timeout_detik); // 30
    println!("Debug default: {}", config2.debug);            // false
}

Enum di JSON #

use serde::{Deserialize, Serialize};

// Enum sederhana — di-serialize sebagai string
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(rename_all = "lowercase")]
enum StatusPesanan {
    Baru,
    Diproses,
    Dikirim,
    Selesai,
    Dibatalkan,
}

// Enum dengan data — berbagai representasi
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "tipe")]  // gunakan field "tipe" sebagai discriminant
enum Notifikasi {
    #[serde(rename = "email")]
    Email { alamat: String, subjek: String },
    #[serde(rename = "sms")]
    SMS { nomor: String },
    #[serde(rename = "push")]
    Push { token: String, judul: String, isi: String },
}

fn main() {
    // Enum string
    let status = StatusPesanan::Dikirim;
    println!("{}", serde_json::to_string(&status).unwrap()); // "dikirim"

    let status2: StatusPesanan = serde_json::from_str(r#""selesai""#).unwrap();
    assert_eq!(status2, StatusPesanan::Selesai);

    // Enum dengan tag
    let notif = Notifikasi::Email {
        alamat: String::from("[email protected]"),
        subjek: String::from("Pesanan Selesai"),
    };
    let json = serde_json::to_string_pretty(&notif).unwrap();
    println!("{}", json);
    // {
    //   "tipe": "email",
    //   "alamat": "[email protected]",
    //   "subjek": "Pesanan Selesai"
    // }
}

serde_json::Value — JSON Dinamis #

Terkadang kamu tidak tahu struktur JSON sebelumnya, atau ingin memanipulasi JSON tanpa mendefinisikan struct. serde_json::Value adalah representasi JSON yang bisa menyimpan tipe apapun:

use serde_json::{json, Value};

fn main() {
    // Buat JSON dengan macro json! — sintaks mirip JSON literal
    let data = json!({
        "nama": "Aplikasi Rust",
        "versi": "1.0.0",
        "fitur": ["json", "database", "api"],
        "konfigurasi": {
            "port": 8080,
            "debug": false
        }
    });

    // Akses field dengan indexing
    println!("Nama: {}", data["nama"]);
    println!("Port: {}", data["konfigurasi"]["port"]);
    println!("Fitur pertama: {}", data["fitur"][0]);

    // get() — mengembalikan Option<&Value>
    if let Some(versi) = data.get("versi") {
        println!("Versi: {}", versi.as_str().unwrap_or("tidak diketahui"));
    }

    // Iterasi array
    if let Some(fitur) = data["fitur"].as_array() {
        for f in fitur {
            println!("- {}", f.as_str().unwrap_or(""));
        }
    }

    // Parse JSON dinamis dari string
    let json_str = r#"{"status": 200, "data": [1, 2, 3], "error": null}"#;
    let nilai: Value = serde_json::from_str(json_str).unwrap();

    match &nilai["status"] {
        Value::Number(n) => println!("Status: {}", n),
        _ => println!("Status tidak diketahui"),
    }

    // Cek tipe nilai
    println!("data adalah array: {}", nilai["data"].is_array());
    println!("error adalah null: {}", nilai["error"].is_null());
}

Modifikasi JSON Dinamis #

use serde_json::{json, Value};

fn main() {
    let mut data: Value = json!({
        "pengguna": {
            "id": 1,
            "nama": "Budi",
            "peran": "user"
        }
    });

    // Modifikasi field
    data["pengguna"]["peran"] = json!("admin");
    data["pengguna"]["terverifikasi"] = json!(true);

    // Tambah field baru
    data["metadata"] = json!({
        "diperbarui": "2024-08-24",
        "versi": 2
    });

    // Hapus field
    if let Some(pengguna) = data["pengguna"].as_object_mut() {
        pengguna.remove("id");
    }

    println!("{}", serde_json::to_string_pretty(&data).unwrap());

    // Gabung dua JSON object
    let tambahan = json!({"email": "[email protected]", "aktif": true});
    if let (Value::Object(base), Value::Object(ext)) =
        (data["pengguna"].as_object_mut().unwrap(), tambahan.as_object().unwrap())
    {
        base.extend(ext.clone());
    }
}

Konversi antara Value dan Struct #

use serde::{Deserialize, Serialize};
use serde_json::{json, Value};

#[derive(Serialize, Deserialize, Debug)]
struct Artikel {
    judul: String,
    penulis: String,
    terbit: String,
}

fn main() {
    // Struct → Value (untuk manipulasi)
    let artikel = Artikel {
        judul: String::from("Belajar Rust"),
        penulis: String::from("Tim Rust"),
        terbit: String::from("2024-01-15"),
    };

    let mut nilai: Value = serde_json::to_value(&artikel).unwrap();
    nilai["views"] = json!(1500);  // tambah field yang tidak ada di struct
    nilai["tags"] = json!(["rust", "programming"]);
    println!("{}", serde_json::to_string_pretty(&nilai).unwrap());

    // Value → Struct (dengan validasi)
    let json_luar = json!({
        "judul": "Panduan Serde",
        "penulis": "Developer Rust",
        "terbit": "2024-06-01",
        "field_ekstra": "diabaikan saat deserialize ke struct"
    });

    let artikel2: Artikel = serde_json::from_value(json_luar).unwrap();
    println!("{:?}", artikel2);
}

Streaming dan File Besar #

Untuk file JSON besar yang tidak muat di memori sekaligus, gunakan streaming deserializer:

use serde::Deserialize;
use serde_json::de::IoRead;
use serde_json::StreamDeserializer;
use std::io::BufReader;
use std::fs::File;

#[derive(Deserialize, Debug)]
struct LogEntry {
    timestamp: u64,
    level: String,
    pesan: String,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // File JSON Lines — setiap baris adalah JSON object terpisah
    // Format: {"timestamp": 1, "level": "INFO", "pesan": "..."}
    // (satu JSON per baris)

    let isi = r#"{"timestamp": 1700000001, "level": "INFO", "pesan": "Server dimulai"}
{"timestamp": 1700000002, "level": "WARN", "pesan": "CPU tinggi"}
{"timestamp": 1700000003, "level": "ERROR", "pesan": "Koneksi gagal"}"#;

    // StreamDeserializer untuk parse banyak JSON dalam satu pass
    let stream = serde_json::Deserializer::from_str(isi).into_iter::<LogEntry>();

    for entry in stream {
        match entry {
            Ok(log) => println!("[{}] {}: {}", log.timestamp, log.level, log.pesan),
            Err(e) => eprintln!("Parse error: {}", e),
        }
    }

    // Serialize banyak item ke stream tanpa memuat semua di memori
    let mut penulis = serde_json::Serializer::new(std::io::stdout());
    let entri = vec![
        LogEntry { timestamp: 1, level: "INFO".into(), pesan: "Test 1".into() },
        LogEntry { timestamp: 2, level: "INFO".into(), pesan: "Test 2".into() },
    ];

    // Serialize sebagai JSON array
    serde::Serialize::serialize(&entri, &mut penulis)?;

    Ok(())
}

Integrasi dengan HTTP — Baca dan Tulis JSON via reqwest #

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }
use serde::{Deserialize, Serialize};
use reqwest;

#[derive(Deserialize, Debug)]
struct JsonPlaceholderPost {
    id: u32,
    title: String,
    body: String,
    #[serde(rename = "userId")]
    user_id: u32,
}

#[derive(Serialize)]
struct PostBaru {
    title: String,
    body: String,
    #[serde(rename = "userId")]
    user_id: u32,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let klien = reqwest::Client::new();

    // GET dan deserialize otomatis
    let post: JsonPlaceholderPost = klien
        .get("https://jsonplaceholder.typicode.com/posts/1")
        .send()
        .await?
        .json()  // otomatis deserialize ke tipe yang ditentukan
        .await?;

    println!("Judul: {}", post.title);
    println!("User ID: {}", post.user_id);

    // GET array of objects
    let semua_post: Vec<JsonPlaceholderPost> = klien
        .get("https://jsonplaceholder.typicode.com/posts")
        .send()
        .await?
        .json()
        .await?;
    println!("Total post: {}", semua_post.len());

    // POST dengan JSON body
    let post_baru = PostBaru {
        title: String::from("Belajar serde_json"),
        body: String::from("serde_json sangat mudah digunakan"),
        user_id: 1,
    };

    let respons = klien
        .post("https://jsonplaceholder.typicode.com/posts")
        .json(&post_baru)  // otomatis serialize dan set Content-Type: application/json
        .send()
        .await?;

    println!("Status POST: {}", respons.status());
    let dibuat: serde_json::Value = respons.json().await?;
    println!("ID baru: {}", dibuat["id"]);

    Ok(())
}

Ringkasan #

  • #[derive(Serialize, Deserialize)] adalah cara termudah dan terpaling umum. Tambahkan ke semua struct yang perlu dikonversi ke/dari JSON.
  • to_string untuk output compact, to_string_pretty untuk output berindentasi — gunakan compact untuk API, pretty untuk debug atau file konfigurasi yang dibaca manusia.
  • from_str, from_slice, from_reader untuk deserialize dari berbagai sumber. from_reader paling efisien untuk file besar karena tidak perlu load semua ke memori.
  • #[serde(rename_all = "camelCase")] di level struct lebih efisien dari #[serde(rename = "...")] di setiap field — berguna untuk API yang menggunakan konvensi camelCase.
  • #[serde(skip_serializing_if = "Option::is_none")] mencegah field None muncul di JSON sebagai null — sangat umum di API yang menggunakan field opsional.
  • #[serde(default)] menggunakan Default::default() jika field tidak ada di JSON. #[serde(default = "fungsi")] menggunakan nilai dari fungsi kustom.
  • serde_json::Value dan macro json! untuk JSON dinamis tanpa struct — gunakan ketika struktur JSON tidak diketahui saat kompilasi atau perlu dimanipulasi secara fleksibel.
  • serde_json::to_value dan from_value untuk konversi antara struct dan Value — berguna untuk menambah field ekstra yang tidak ada di struct.
  • Untuk file JSON besar, gunakan StreamDeserializer (JSON Lines) atau from_reader dengan BufReader untuk menghindari load semua data ke memori.

← Sebelumnya: Mocking   Berikutnya: YAML →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact