Search code examples
rustserdeserde-json

Functionally creating a nested object from a flat structure


I am attempting to turn a flat structure like the following:

let flat = vec![
    Foo {
        a: "abc1".to_owned(),
        b: "efg1".to_owned(),
        c: "yyyy".to_owned(),
        d: "aaaa".to_owned(),
    },
    Foo {
        a: "abc1".to_owned(),
        b: "efg2".to_owned(),
        c: "zzzz".to_owned(),
        d: "bbbb".to_owned(),
    }];

into a nested JSON object through serde_json that looks something like:

{
    "abc1": {
        "efg1": {
            "c": "hij1",
            "d": "aaaa", 
        },
        "efg2": {
            "c": "zzzz",
            "d": "bbbb", 
        },
    }
}

(The values b are guaranteed to be unique within the array)

If I had needed only one layer, I would do something like this:

let map = flat.into_iter().map(|input| (input.a, NewType {
    b: input.b,
    c: input.c,
    d: input.d,
})).collect::<Hashmap<String, NewType>>();

let out = serde_json::to_string(map).unwrap();

However, this doesn't seem to scale to multiple layers (i.e. (String, (String, NewType)) can't collect into Hashmap<String, Hashmap<String, NewType>>)

Is there a better way than manually looping and inserting entries into the hashmaps, before turning them into json?


Solution

  • A map will preserve the shape of the data. That is not what you want; the cardinality of the data has been changed after the transformation. So a mere map won't be sufficient.

    Instead, a fold will do: you start with an empty HashMap, and populate it as you iterate through the collection. But it is hardly any more readable than a loop in this case. I find a multimap is quite useful here:

    use multimap::MultiMap;
    use std::collections::HashMap;
    
    struct Foo {
        a: String,
        b: String,
        c: String,
        d: String,
    }
    
    #[derive(Debug)]
    struct NewFoo {
        c: String,
        d: String,
    }
    
    fn main() {
        let flat = vec![
            Foo {
                a: "abc1".to_owned(),
                b: "efg1".to_owned(),
                c: "yyyy".to_owned(),
                d: "aaaa".to_owned(),
            },
            Foo {
                a: "abc1".to_owned(),
                b: "efg2".to_owned(),
                c: "zzzz".to_owned(),
                d: "bbbb".to_owned(),
            },
        ];
        let map = flat
            .into_iter()
            .map(|e| (e.a, (e.b, NewFoo { c: e.c, d: e.d })))
            .collect::<MultiMap<_, _>>()
            .into_iter()
            .map(|e| (e.0, e.1.into_iter().collect::<HashMap<_, _>>()))
            .collect::<HashMap<_, _>>();
        println!("{:#?}", map);
    }