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(&notif).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_string for compact output, to_string_pretty for indented output — use compact for APIs, pretty for debugging or human-readable config files.
  • from_str, from_slice, from_reader for deserializing from various sources. from_reader is 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")] prevents None fields from appearing in JSON as null — very common in APIs using optional fields.
  • #[serde(default)] uses Default::default() if a field is absent in the JSON. #[serde(default = "fungsi")] uses the value from a custom function.
  • serde_json::Value and the json! macro for dynamic JSON without structs — use when the JSON structure isn’t known at compile time or needs flexible manipulation.
  • serde_json::to_value and from_value for conversion between structs and Value — useful for adding extra fields not in the struct.
  • For large JSON files, use StreamDeserializer (JSON Lines) or from_reader with BufReader to avoid loading all data into memory.

← Previous: Mocking   Next: YAML →

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