RustArena

Step 5 of 6

Dynamic JSON with Value and json!

Sometimes you don't want a struct: you only need one field from a big document, the shape varies, or you're building a quick response. For that, serde_json has serde_json::Value, an enum that can hold any JSON:

enum Value {
    Null,
    Bool(bool),
    Number(Number),
    String(String),
    Array(Vec<Value>),
    Object(Map<String, Value>),
}
use serde_json::{json, Value};

fn main() {
    let text = r#"{"user": {"name": "Ferris", "tags": ["crab", "admin"]}, "visits": 12}"#;
    let v: Value = serde_json::from_str(text).unwrap();

    println!("{}", v["user"]["name"]);
    println!("{:?}", v["user"]["name"].as_str());
    println!("{}", v["visits"].as_u64().unwrap_or(0) + 1);
    println!("{}", v["user"]["email"]);
    println!("{}", v["user"]["tags"][1]);

    let reply = json!({ "ok": true, "visits": v["visits"], "id": 7 });
    println!("{reply}");
}
"Ferris"
Some("Ferris")
13
null
"admin"
{"id":7,"ok":true,"visits":12}

Piece by piece:

Value is an ordinary enum, so you can match on it:

use serde_json::Value;

fn describe(v: &Value) -> String {
    match v {
        Value::Null => String::from("nothing"),
        Value::Bool(b) => format!("yes/no: {b}"),
        Value::Number(n) => format!("number {n}"),
        Value::String(s) => format!("text {s}"),
        Value::Array(items) => format!("list of {}", items.len()),
        Value::Object(map) => format!("object with {} keys", map.len()),
    }
}

fn main() {
    let v: Value = serde_json::from_str(r#"[null, true, 2.5, "hi", [1, 2], {"a": 1}]"#).unwrap();
    for item in v.as_array().unwrap() {
        println!("{}", describe(item));
    }
}
nothing
yes/no: true
number 2.5
text hi
list of 2
object with 1 keys

You can also convert between the two worlds: serde_json::to_value(&my_struct) and serde_json::from_value::<MyStruct>(value).

When to use which

Prefer structs: the compiler checks every field access, and a wrong shape is reported in one place, at parse time. Use Value for genuinely dynamic data (arbitrary user metadata, proxying JSON you don't own, picking one field out of a huge response) and for tests, where json! makes expected values easy to write.

Common mistakes

Your task

Write fn extract_emails(json: &str) -> Vec<String>. The input looks like {"users": [{"email": "[email protected]"}, {"name": "no email"}, ...]}. Return every email that is a string, in order, skipping users without one. If the input isn't valid JSON, or users isn't an array, return an empty Vec.

Challenge 2

Build a response with json!. Write fn stats(scores: &[i64]) -> Value returning {"count": <number of scores>, "max": <largest score>, "min": <smallest score>}, where max and min are null when the slice is empty. (Hint: an Option inside json! becomes the value or null.)

Challenge 3

Write a program that reads one JSON object from standard input and prints each top-level key with the kind of its value (null, bool, number, string, array or object), one per line as key: kind, in sorted key order. If the input isn't a JSON object, print not an object.

Previous: Enums in JSON Next: Bad input, validation and other formats