JSON #
JSON is the almost-universal data exchange format in the modern web world — nearly every REST API, configuration file, and communication protocol uses it. In Rust, all JSON needs are handled by two crates working together: serde as the generic serialization/deserialization framework, and serde_json as its implementation for the JSON format. This combination is very ergonomic: just add #[derive(Serialize, Deserialize)] to a struct, and the entire conversion mechanism between Rust structs and JSON works automatically — without writing a single line of boilerplate. This article covers everything from the most basic usage to advanced patterns like dynamic JSON, streaming, and field transformation.
Installation #
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
The "derive" feature on serde enables the #[derive(Serialize, Deserialize)] proc-macro — without it you’d have to implement those traits manually.
Basic Serialization and Deserialization #
Struct to 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 to a compact JSON string
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 to a pretty-printed JSON string (indented)
let json_cantik = serde_json::to_string_pretty(&pengguna).unwrap();
println!("{}", json_cantik);
// Serialize to Vec<u8> (bytes) — useful for sending over the network
let json_bytes = serde_json::to_vec(&pengguna).unwrap();
println!("Size: {} bytes", json_bytes.len());
}
JSON to 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 from &str
let produk: Produk = serde_json::from_str(json_str).unwrap();
println!("{:?}", produk);
// Deserialize from &[u8] (bytes)
let json_bytes = json_str.as_bytes();
let produk2: Produk = serde_json::from_slice(json_bytes).unwrap();
println!("Price: Rp{:.0}", produk2.harga);
// Deserialize from io::Read (e.g. a file)
let file = std::fs::File::open("produk.json");
// let produk3: Produk = serde_json::from_reader(file.unwrap()).unwrap();
// Proper error handling — don't unwrap in production
match serde_json::from_str::<Produk>(r#"{"id": "not a number"}"#) {
Ok(p) => println!("{:?}", p),
Err(e) => println!("Parse error: {} at line {}, column {}", e, e.line(), e.column()),
}
}
Struct with Both Traits at Once #
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 succeeded: {:?}", kembali);
}
#[serde(...)] Annotations — Controlling the JSON Format
#
Serde provides a rich set of annotations to control how fields are serialized and deserialized without changing the Rust field names.
rename — Different Field Names in JSON
#
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
struct ResponAPI {
// Rust name: id_pengguna, JSON name: "userId"
#[serde(rename = "userId")]
id_pengguna: u64,
#[serde(rename = "fullName")]
nama_lengkap: String,
#[serde(rename = "isActive")]
aktif: bool,
// The camelCase convention for the whole struct can be done at the struct level
#[serde(rename = "createdAt")]
dibuat_pada: String,
}
// A more efficient way: rename_all at the struct level
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")] // all fields are automatically renamed to 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, and skip_serializing_if
#
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
struct Konfigurasi {
host: String,
port: u16,
// Not included during serialization (doesn't appear in the JSON output)
#[serde(skip_serializing)]
password: String,
// Not included at all (also ignored when parsing)
#[serde(skip)]
cache_internal: Option<String>,
// Not included in JSON if the value is None
#[serde(skip_serializing_if = "Option::is_none")]
komentar: Option<String>,
// Use a default value if the field is absent in the JSON
#[serde(default)]
debug: bool, // defaults to false if absent in the 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, // will be skipped in the JSON output
debug: false,
timeout_detik: 30,
};
let json = serde_json::to_string_pretty(&config).unwrap();
println!("{}", json);
// password doesn't appear, cache_internal doesn't appear, komentar doesn't appear
// Deserialize: missing fields use their defaults
let json_minimal = r#"{"host": "db.contoh.com", "port": 5432, "password": ""}"#;
let config2: Konfigurasi = serde_json::from_str(json_minimal).unwrap();
println!("Default timeout: {}s", config2.timeout_detik); // 30
println!("Default debug: {}", config2.debug); // false
}
Enums in JSON #
use serde::{Deserialize, Serialize};
// Simple enum — serialized as a string
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(rename_all = "lowercase")]
enum StatusPesanan {
Baru,
Diproses,
Dikirim,
Selesai,
Dibatalkan,
}
// Enum with data — various representations
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "tipe")] // use the "tipe" field as the 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() {
// String enum
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 with a tag
let notif = Notifikasi::Email {
alamat: String::from("[email protected]"),
subjek: String::from("Order Complete"),
};
let json = serde_json::to_string_pretty(¬if).unwrap();
println!("{}", json);
// {
// "tipe": "email",
// "alamat": "[email protected]",
// "subjek": "Order Complete"
// }
}
serde_json::Value — Dynamic JSON
#
Sometimes you don’t know the JSON structure in advance, or you want to manipulate JSON without defining a struct. serde_json::Value is a JSON representation that can hold any type:
use serde_json::{json, Value};
fn main() {
// Create JSON with the json! macro — syntax similar to a JSON literal
let data = json!({
"nama": "Aplikasi Rust",
"versi": "1.0.0",
"fitur": ["json", "database", "api"],
"konfigurasi": {
"port": 8080,
"debug": false
}
});
// Access fields with indexing
println!("Name: {}", data["nama"]);
println!("Port: {}", data["konfigurasi"]["port"]);
println!("First feature: {}", data["fitur"][0]);
// get() — returns Option<&Value>
if let Some(versi) = data.get("versi") {
println!("Version: {}", versi.as_str().unwrap_or("unknown"));
}
// Iterate an array
if let Some(fitur) = data["fitur"].as_array() {
for f in fitur {
println!("- {}", f.as_str().unwrap_or(""));
}
}
// Parse dynamic JSON from a 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 unknown"),
}
// Check value types
println!("data is an array: {}", nilai["data"].is_array());
println!("error is null: {}", nilai["error"].is_null());
}
Modifying Dynamic JSON #
use serde_json::{json, Value};
fn main() {
let mut data: Value = json!({
"pengguna": {
"id": 1,
"nama": "Budi",
"peran": "user"
}
});
// Modify fields
data["pengguna"]["peran"] = json!("admin");
data["pengguna"]["terverifikasi"] = json!(true);
// Add new fields
data["metadata"] = json!({
"diperbarui": "2024-08-24",
"versi": 2
});
// Remove a field
if let Some(pengguna) = data["pengguna"].as_object_mut() {
pengguna.remove("id");
}
println!("{}", serde_json::to_string_pretty(&data).unwrap());
// Merge two JSON objects
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());
}
}
Converting Between Value and 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 (for manipulation)
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); // add a field not in the struct
nilai["tags"] = json!(["rust", "programming"]);
println!("{}", serde_json::to_string_pretty(&nilai).unwrap());
// Value → Struct (with validation)
let json_luar = json!({
"judul": "Panduan Serde",
"penulis": "Developer Rust",
"terbit": "2024-06-01",
"field_ekstra": "ignored when deserializing into a struct"
});
let artikel2: Artikel = serde_json::from_value(json_luar).unwrap();
println!("{:?}", artikel2);
}
Streaming and Large Files #
For large JSON files that don’t fit in memory at once, use a 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>> {
// JSON Lines file — each line is a separate JSON object
// Format: {"timestamp": 1, "level": "INFO", "pesan": "..."}
// (one JSON per line)
let isi = r#"{"timestamp": 1700000001, "level": "INFO", "pesan": "Server started"}
{"timestamp": 1700000002, "level": "WARN", "pesan": "High CPU"}
{"timestamp": 1700000003, "level": "ERROR", "pesan": "Connection failed"}"#;
// StreamDeserializer to parse many JSONs in a single 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 many items to a stream without loading everything into memory
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 as a JSON array
serde::Serialize::serialize(&entri, &mut penulis)?;
Ok(())
}
HTTP Integration — Reading and Writing JSON with 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 with automatic deserialization
let post: JsonPlaceholderPost = klien
.get("https://jsonplaceholder.typicode.com/posts/1")
.send()
.await?
.json() // automatically deserializes into the specified type
.await?;
println!("Title: {}", post.title);
println!("User ID: {}", post.user_id);
// GET an array of objects
let semua_post: Vec<JsonPlaceholderPost> = klien
.get("https://jsonplaceholder.typicode.com/posts")
.send()
.await?
.json()
.await?;
println!("Total posts: {}", semua_post.len());
// POST with a JSON body
let post_baru = PostBaru {
title: String::from("Belajar serde_json"),
body: String::from("serde_json is very easy to use"),
user_id: 1,
};
let respons = klien
.post("https://jsonplaceholder.typicode.com/posts")
.json(&post_baru) // automatically serializes and sets Content-Type: application/json
.send()
.await?;
println!("POST status: {}", respons.status());
let dibuat: serde_json::Value = respons.json().await?;
println!("New ID: {}", dibuat["id"]);
Ok(())
}
Summary #
#[derive(Serialize, Deserialize)]is the easiest and most common way. Add it to every struct that needs conversion to/from JSON.to_stringfor compact output,to_string_prettyfor indented output — use compact for APIs, pretty for debugging or human-readable config files.from_str,from_slice,from_readerfor deserializing from various sources.from_readeris most efficient for large files since it doesn’t load everything into memory.#[serde(rename_all = "camelCase")]at the struct level is more efficient than#[serde(rename = "...")]on every field — useful for APIs using the camelCase convention.#[serde(skip_serializing_if = "Option::is_none")]preventsNonefields from appearing in JSON asnull— very common in APIs using optional fields.#[serde(default)]usesDefault::default()if a field is absent in the JSON.#[serde(default = "fungsi")]uses the value from a custom function.serde_json::Valueand thejson!macro for dynamic JSON without structs — use when the JSON structure isn’t known at compile time or needs flexible manipulation.serde_json::to_valueandfrom_valuefor conversion between structs andValue— useful for adding extra fields not in the struct.- For large JSON files, use
StreamDeserializer(JSON Lines) orfrom_readerwithBufReaderto avoid loading all data into memory.