Skip to Content
Course content

166: Working with serde_json Value

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're sorting through a big box of mixed mail. You've got letters, postcards, thick envelopes with bills, and maybe a few glossy brochures. Before you can actually do anything with a piece of mail, you have to look at it and figure out what it is. Is this a letter? If so, I can read the text. Is this a bill? If so, I can look for the "Amount Due" field. You can't just treat everything as a bill, because trying to find an "Amount Due" on a postcard doesn't make sense.

That's exactly how serde_json::Value works. Up until now, we've been using strongly typed structs to deserialize JSON. That's great when you know exactly what the API is sending. But in the real world, you'll often run into "schemaless" or dynamic JSON—where the keys might change, or the value could be a string one time and a number the next. In those cases, we use Value, which is essentially a giant enum that says: "This is either an Object, an Array, a String, a Number, a Boolean, or Null."

When you can't trust the schema

I've spent a lot of time working with legacy APIs where the documentation is a lie. You'll see a field called metadata that is sometimes a simple string, sometimes a complex object, and sometimes just null. If you try to map that to a strict Rust struct, your program will crash (or rather, fail to deserialize) the moment the API sends something unexpected.

By deserializing into serde_json::Value, you're telling Rust: "I don't know what this is yet. Just hold onto the data, and I'll figure it out at runtime."

use serde_json::Value;

fn main() {
    let data = r#"
        {
            "name": "Project Phoenix",
            "version": 2,
            "tags": ["rust", "json", "dynamic"],
            "settings": {
                "retries": 3,
                "enabled": true
            }
        }
    "#;

    // We parse into Value instead of a custom struct
    let v: Value = serde_json::from_str(data).unwrap();

    // Now we can access fields dynamically
    println!("Project: {}", v["name"]); 
}

Peeking inside the Value enum

The Value type implements the Index trait, which is why I could use v["name"] above. It feels like JavaScript, but there's a catch: if the key doesn't exist, it returns a Value::Null. It won't panic immediately, but it might cause issues later if you assume the data is there.

If you want to actually use the data as a Rust type (like a &str or an i64), you have to use the as_... methods. These return an Option, forcing you to handle the case where the data isn't what you thought it was.

// Accessing a string safely
if let Some(name) = v["name"].as_str() {
    println!("The name is {}", name);
}

// Accessing a nested number
if let Some(retries) = v["settings"]["retries"].as_i64() {
    println!("Retries set to: {}", retries);
}

// Handling arrays
if let Some(tags) = v["tags"].as_array() {
    for tag in tags {
        println!("Tag: {}", tag);
    }
}

The danger of the panic-prone index

Here is a bit of a warning. While v["key"] is convenient, it's a bit "loose." If you're writing production code and you need to be absolutely sure a key exists before you try to manipulate it, I recommend using .get(). .get() returns an Option<&Value>, which is much cleaner when you're chaining lookups.

I usually prefer .get() over [] because it makes the possibility of failure explicit. In a large codebase, seeing an Option tells the next developer (which might be you in six months) that this piece of JSON isn't guaranteed to be there.

// Instead of this (which returns Value::Null if missing):
let val = &v["missing_key"]; 

// Do this (which returns None if missing):
if let Some(val) = v.get("missing_key") {
    println!("Found it: {}", val);
} else {
    println!("Key was missing entirely.");
}



📋 Practical Task

Build a Dynamic API Response Filter

You are building a tool that processes responses from a chaotic legacy API. The API returns a JSON object where some fields are consistent, but others are nested in a metadata object that changes structure depending on the request.

Your Task: Write a function extract_priority(json_data: &str) -> Option<i64> that does the following:

  • Parses the input string into a serde_json::Value.
  • Checks if there is a top-level field called "priority". If it exists and is a number, return it.
  • If "priority" is not at the top level, look inside a field called "metadata". If "metadata" is an object and contains a field called "priority" that is a number, return that.
  • If neither exists or the value is not a number, return None.

Test your function with these three cases:

  1. {"priority": 10, "name": "Task A"} → Should return Some(10)
  2. {"name": "Task B", "metadata": {"priority": 5, "source": "web"}} → Should return Some(5)
  3. {"name": "Task C", "metadata": {"tags": ["urgent"]}} → Should return None
Rating
0 0

There are no comments for now.

to be the first to leave a comment.