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(¬if).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_stringuntuk output compact,to_string_prettyuntuk output berindentasi — gunakan compact untuk API, pretty untuk debug atau file konfigurasi yang dibaca manusia.from_str,from_slice,from_readeruntuk deserialize dari berbagai sumber.from_readerpaling 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 fieldNonemuncul di JSON sebagainull— sangat umum di API yang menggunakan field opsional.#[serde(default)]menggunakanDefault::default()jika field tidak ada di JSON.#[serde(default = "fungsi")]menggunakan nilai dari fungsi kustom.serde_json::Valuedan macrojson!untuk JSON dinamis tanpa struct — gunakan ketika struktur JSON tidak diketahui saat kompilasi atau perlu dimanipulasi secara fleksibel.serde_json::to_valuedanfrom_valueuntuk konversi antara struct danValue— berguna untuk menambah field ekstra yang tidak ada di struct.- Untuk file JSON besar, gunakan
StreamDeserializer(JSON Lines) ataufrom_readerdenganBufReaderuntuk menghindari load semua data ke memori.