Search code examples
rustserde-json

Stumped with serde_json map


I am trying to generate json object as below

// final json output expected
{
    "j1": {
        "1981": {
            "income": 215
        },
        "1982": {
            "income": 350
        },
    },
    "j2": {
        "1981": {
            "income": 100
        },
        "1982": {
            "income": 215
        },
    }
}

I have the following code

use serde_json::{Value, Map};

struct s {
    name: String,
    year: i32,
    income: i64
}

fn main() {
    let rows :Vec<s> = vec!(
        s { name: "j1".to_string(), year: 1981, income: 100},
        s { name: "j1".to_string(), year: 1981, income: 115},
        s { name: "j1".to_string(), year: 1982, income: 120},
        s { name: "j1".to_string(), year: 1982, income: 100},
        s { name: "j1".to_string(), year: 1982, income: 130},
        s { name: "j2".to_string(), year: 1981, income: 100},
        s { name: "j2".to_string(), year: 1982, income: 120},
        s { name: "j2".to_string(), year: 1982, income: 130}
    );
    
    let mut data_map: Map<String, Value> = Map::new();
    for row in rows {
        let  mut name: &Map<String, Value> = match data_map.get(&row.name) {
            Some(val) => { &val.as_object().unwrap() },
            None => { &Map::new() }
        };
        let mut year:  &Map<String, Value> = match data_map.get(&row.year.to_string()) {
            Some(val) => { &val.as_object().unwrap() },
            None => { &Map::new() }
        };
        let income: i64 = year.get("income").unwrap_or(&Value::Number(0i64.into())).as_i64().unwrap() + row.income;
        year.insert("income".to_string(), Value::Number(income.into()));
    }
}

I created this code to ask the question. I also consulted with deepseek and such

Error message that I am getting is

error[E0596]: cannot borrow `*year` as mutable, as it is behind a `&` reference
  --> src\main.rs:32:9
   |
32 |         year.insert("income".to_string(), Value::Number(income.into()));
   |         ^^^^ `year` is a `&` reference, so the data it refers to cannot be borrowed as mutable
   |
help: consider changing this binding's type
   |
27 |         let mut year: &mut serde_json::Map<std::string::String, Value> = match data_map.get(&row.year.to_string()) {
   |                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Clearly I have deficiency in understanding reference and borrowing. Any help would be appreciated.


Solution

  • First off, you don't usually create JSON by hand. You create a Rust representation of it, and then let serde_json convert it to JSON. That means, don't deal with Value yourself.

    You can of course manually construct JSONs as well, but it rarely has an advantage over letting serde_json do that.

    Here's a solution. First, we need the following dependencies in Cargo.toml:

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

    And here is the code:

    use serde::{Deserialize, Serialize};
    
    use std::collections::HashMap;
    
    struct S {
        name: String,
        year: i32,
        income: i64,
    }
    
    #[derive(Serialize, Deserialize, Debug, Clone)]
    struct YearData {
        income: i64,
    }
    
    fn main() {
        let rows: Vec<S> = vec![
            S { name: "j1".to_string(), year: 1981, income: 100 },
            S { name: "j1".to_string(), year: 1981, income: 115 },
            S { name: "j1".to_string(), year: 1982, income: 120 },
            S { name: "j1".to_string(), year: 1982, income: 100 },
            S { name: "j1".to_string(), year: 1982, income: 130 },
            S { name: "j2".to_string(), year: 1981, income: 100 },
            S { name: "j2".to_string(), year: 1982, income: 120 },
            S { name: "j2".to_string(), year: 1982, income: 130 },
        ];
    
        let mut data_map: HashMap<String, HashMap<String, YearData>> = HashMap::new();
        for row in rows {
            data_map
                .entry(row.name)
                .or_default()
                .entry(row.year.to_string())
                .or_insert(YearData { income: 0 })
                .income += row.income;
        }
    
        let serde_string = serde_json::to_string_pretty(&data_map).unwrap();
    
        println!("{}", serde_string);
    }
    
    {
      "j1": {
        "1982": {
          "income": 350
        },
        "1981": {
          "income": 215
        }
      },
      "j2": {
        "1981": {
          "income": 100
        },
        "1982": {
          "income": 250
        }
      }
    }
    

    Regarding the borrowing error in your original code: I'm uncertain how to help you there. There are many errors in there, both ownership issues as also logical issues.

    I assume that you intend to query a member from data_map by using the row.name key, and if it doesn't exist, insert a new map. I see that you create a new map, but you never insert it into data_map anywhere. The rest of the issues are a bunch of & that are wrong, sometimes because the values are already references, and sometimes because they are towards variables that are temporary and you create dangling references.

    I'd love to dissect your code in more detail and explain the errors to you, but it's kind of too confusing to do so :D

    Instead, here's a rewrite. Maybe it helps your understanding:

    use serde_json::{Map, Value};
    
    struct S {
        name: String,
        year: i32,
        income: i64,
    }
    
    fn main() {
        let rows: Vec<S> = vec![
            S { name: "j1".to_string(), year: 1981, income: 100 },
            S { name: "j1".to_string(), year: 1981, income: 115 },
            S { name: "j1".to_string(), year: 1982, income: 120 },
            S { name: "j1".to_string(), year: 1982, income: 100 },
            S { name: "j1".to_string(), year: 1982, income: 130 },
            S { name: "j2".to_string(), year: 1981, income: 100 },
            S { name: "j2".to_string(), year: 1982, income: 120 },
            S { name: "j2".to_string(), year: 1982, income: 130 },
        ];
    
        let mut data_map: Map<String, Value> = Map::new();
        for row in rows {
            let name: &mut Map<String, Value> = data_map
                .entry(row.name)
                .or_insert_with(|| Map::new().into())
                .as_object_mut()
                .unwrap();
            let year: &mut Map<String, Value> = name
                .entry(row.year.to_string())
                .or_insert_with(|| Map::new().into())
                .as_object_mut()
                .unwrap();
            let income: i64 = year
                .get("income")
                .unwrap_or(&Value::Number(0i64.into()))
                .as_i64()
                .unwrap()
                + row.income;
            year.insert("income".to_string(), Value::Number(income.into()));
        }
    
        println!("{}", serde_json::to_string_pretty(&data_map).unwrap());
    }
    
    {
      "j1": {
        "1981": {
          "income": 215
        },
        "1982": {
          "income": 350
        }
      },
      "j2": {
        "1981": {
          "income": 100
        },
        "1982": {
          "income": 250
        }
      }
    }
    

    But again, don't do this unless absolutely necessary; serde_json is not really meant to be used like this. Using Value directly just causes a lot of as_<type>().unwrap() casts, and there's no real advantage over using a Rust type that implements serde::Serialize. This is how serde_json is meant to be used; see #[derive(Serialize)] in my first example. I recommend reading serde.rs for more information.