Search code examples
jsonrustserdeserde-json

How to deserialize a JSON input as a generic datastructure in Rust?


Most examples showing deserialization of JSON with Rust and serde_json show deserialization into a known structure format, using a defined struct.

Is it possible to deserialize a JSON document with an unknown structure into a generic Rust datatype which can handle this structural variance?

For example, I tried something like this

let filename = "example.json";
let maybe_ifile = std::fs::File::open(filename);

match maybe_ifile {
    Ok(ifile) => {
        let maybe_json_data = serde_json::from_reader::<_, BTreeMap<String, String>>(ifile);

        match maybe_json_data {
            Ok(json_data) => {
                println!("{json_data:?}");
            }

            Err(error) => {
                panic!("{error}")
            }
        }
    }

    Err(error) => {
        panic!("{error}");
    }
}

This works if the JSON document is structured as pairs of key-value strings, but it does not work for more general structures, so clearly using BTreeMap<String, String> is not the right approach here.

What I am trying to achieve is flexibility using runtime / somewhat dynamic types.

This is possible in other languages, so I would assume there is a fairly trivial solution in Rust, I am just not aware of what this is.


Solution

  • The solution was to use the serde_json::Value datatype. This provides a generic way to work with JSON data.

    The single line changed:

    let maybe_json_data = serde_json::from_reader::<_, serde_json::Value>(ifile);
    

    Example deserialize generic JSON Document

    Here's an example of deserializing a JSON string into a serde_json::Value:

    use serde_json::Value;
    
    fn main() -> Result<(), Box<dyn std::error::Error>> {
        // Example JSON string
        let json_str = r#"
        {
            "name": "Alice",
            "age": 30,
            "is_member": true,
            "favorites": {
                "colors": ["red", "green", "blue"],
                "numbers": [1, 2, 3]
            }
        }
        "#;
    
        // Parse the JSON string into a serde_json::Value
        let json_value: Value = serde_json::from_str(json_str)?;
    
        // Accessing elements in the JSON
        if let Some(name) = json_value.get("name").and_then(|v| v.as_str()) {
            println!("Name: {}", name);
        }
    
        if let Some(age) = json_value.get("age").and_then(|v| v.as_i64()) {
            println!("Age: {}", age);
        }
    
        if let Some(is_member) = json_value.get("is_member").and_then(|v| v.as_bool()) {
            println!("Is Member: {}", is_member);
        }
    
        // Nested JSON objects
        if let Some(favorites) = json_value.get("favorites") {
            if let Some(colors) = favorites.get("colors").and_then(|v| v.as_array()) {
                println!("Favorite Colors: {:?}", colors);
            }
        }
    
        Ok(())
    }
    

    Explanation

    Parsing JSON: The serde_json::from_str function parses the JSON string into a serde_json::Value.

    Accessing Data: Use the get method to access fields in the JSON. You can further use type-specific methods like as_str, as_i64, and as_bool to convert the JSON values to Rust types.

    Handling Nested and Complex JSON

    The serde_json::Value enum can represent JSON data of any structure:

    • Value::Object for JSON objects
    • Value::Array for arrays
    • Value::String for strings
    • Value::Number for numbers
    • Value::Bool for booleans
    • Value::Null for nulls

    This makes it suitable for generic JSON deserialization when the structure is not fixed.