RustArena

Serde JSON in Rust: serialize and deserialize structs

How to convert Rust structs to and from JSON with serde and serde_json: derive, to_string, from_str, rename, Option fields, defaults and errors.

serde (SERialize/DEserialize) is the standard Rust framework for converting data to and from formats like JSON, TOML and YAML. You describe your data as normal structs and enums, derive two traits, and a format crate such as serde_json does the rest.

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

Struct to JSON and back

use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct Player {
    name: String,
    level: u32,
    tags: Vec<String>,
}

fn main() {
    let p = Player { name: "Ferris".into(), level: 7, tags: vec!["crab".into()] };
    let json = serde_json::to_string(&p).unwrap();
    println!("{json}");

    let back: Player = serde_json::from_str(r#"{"name":"Corro","level":3,"tags":[]}"#).unwrap();
    println!("{back:?}");
}
{"name":"Ferris","level":7,"tags":["crab"]}
Player { name: "Corro", level: 3, tags: [] }

r#"..."# is a raw string: quotes inside don't need escaping, which is handy for JSON in code.

Shaping the JSON with attributes

Attribute Effect
#[serde(rename_all = "camelCase")] on the struct user_id becomes "userId"
#[serde(rename = "type")] on a field use a JSON name that's a Rust keyword
Option<T> field null or a missing key becomes None
#[serde(default)] missing key uses Default::default()
#[serde(skip_serializing_if = "Option::is_none")] leave out None fields when writing
#[serde(skip)] never read or write this field
#[serde(tag = "type")] on an enum internally tagged: {"type": "Click", "x": 1}
#[serde(deny_unknown_fields)] reject keys you didn't expect

By default, unknown JSON keys are ignored, and a missing non-Option field is an error.

Handling errors

serde_json::Error says what went wrong and where:

Match on the Result, or propagate with ? into your error type (with anyhow, add context: .context("parsing config.json")).

Untyped JSON: Value and json!

When the shape isn't known in advance, parse into serde_json::Value and index it: v["user"]["name"].as_str(). The json! macro builds a Value with JSON-like syntax. Prefer typed structs for anything you'll use more than once: the compiler then checks every field access.

Common mistakes

Example

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

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Order {
    order_id: u64,
    customer: String,
    items: Vec<Item>,
    #[serde(default)]
    gift_wrap: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    coupon: Option<String>,
}

#[derive(Debug, Serialize, Deserialize)]
struct Item {
    sku: String,
    qty: u32,
    price_cents: u64,
}

fn main() {
    let input = r#"{
        "orderId": 1042,
        "customer": "Ferris",
        "items": [
            {"sku": "MUG-01", "qty": 2, "price_cents": 1250},
            {"sku": "TEE-XL", "qty": 1, "price_cents": 2000}
        ]
    }"#;

    let order: Order = serde_json::from_str(input).expect("valid order JSON");
    let total: u64 = order.items.iter().map(|i| u64::from(i.qty) * i.price_cents).sum();
    println!("order {} for {}: {} items, total {total} cents, gift wrap {}", order.order_id, order.customer, order.items.len(), order.gift_wrap);
    println!("first sku {}", order.items[0].sku);

    println!("{}", serde_json::to_string(&order).unwrap());

    for bad in [r#"{"orderId": 1}"#, r#"{"orderId": "x", "customer": "a", "items": []}"#, "<html>502</html>"] {
        match serde_json::from_str::<Order>(bad) {
            Ok(o) => println!("parsed {:?}", o.coupon),
            Err(e) => println!("error: {e}"),
        }
    }

    let event = json!({ "type": "checkout", "order": order.order_id, "ok": true });
    println!("{} {}", event, event["order"].as_u64().unwrap_or(0));
}

Output:

order 1042 for Ferris: 2 items, total 4500 cents, gift wrap false
first sku MUG-01
{"orderId":1042,"customer":"Ferris","items":[{"sku":"MUG-01","qty":2,"price_cents":1250},{"sku":"TEE-XL","qty":1,"price_cents":2000}],"giftWrap":false}
error: missing field `customer` at line 1 column 14
error: invalid type: string "x", expected u64 at line 1 column 15
error: expected value at line 1 column 1
{"ok":true,"order":1042,"type":"checkout"} 1042

Practice it