Search code examples
jsonloopsrusthashmapkey

Trying to loop getting key-value pairs from a json file in Rust to populate a map


I'm still fairly new to rust (coming from PHP), currently trying to teach myself trial by fire by working on a passion project in it.

For one of my mods in the code, I need to process a runtime supplied json file to be sent off to an another externally used format that requires pulling data out of the format & restructuring it.

Here is my current code:

use {
    std::{
        fs::*,
        path::Path,
        collections::{HashMap},
    },
    ajson::*,
    serde_json::*,
    
}

pub async fn process() -> HashMap<String, Object> {
    let mut return_hash: HashMap<String, Object> = HashMap::new();
    let path = "./downloadFolder/APIReturn/a33562.json";
    let json = &std::fs::read_to_string(path).expect("Unable to read file");

    let default_map = ajson::get(json, "Defaults.Settings").expect("JSON was not well-formatted");
    let indince_map = ajson::get(json, "indinces").expect("JSON was not well-formatted");


    let mut loop_count = 0;
    for (key, value) in serde_json::json!(indince_map){
         return_hash.insert(key.to_string(), indince::new(key, value));
    }

I'm trying to loop through a json with a format like the following:

{
    "Defaults":{
        "settings": {
             "Angle": 90,
             "Region": "27B",
             "Area": 187
        }
    },
    "indinces": {
        "Springfield": {
             "Angle": 45,
             "Area": 285
        }, 
        "Lawrence": {
             "Region": "38A",
        },
        ...
    }
}

The loop should be iterating through the json values, giving key/value pairs like....

key: "Springfield", value: Object<{angle: 45, area:285}>

key: "Lawrence", value: Object<{region: 38A}>

And then I'll be later populating those objects with some calculated values and defaults values for anything missed and passing them on.

However, this particular section of code gives me the error:

for (key, value) in serde_json::json!(indince_map){
    |                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |                         |
    |                         the trait `Serialize` is not implemented for `ajson::Value<'_>`
    |                         required by a bound introduced by this call

But I've tried serializing the json, parsing it, mapping it, cloning and unwrapping, and a bunch of other things that seem to just not work, and I just keep getting errors or results that aren't giving me what I want.

I honestly feel like I have no clue what I need to do at this point.


Solution

  • It looks like you are trying to use JSON manually, which is possible, but very cumbersome.

    serde is primarily meant to be used to parse into existing structs, so I recommend creating a struct that represents your data and parsing into that one.

    Like so, for example:

    use std::collections::HashMap;
    
    use serde::Deserialize;
    
    const DATA: &str = r#"{
        "Defaults":{
            "settings": {
                 "Angle": 90,
                 "Region": "27B",
                 "Area": 187
            }
        },
        "indinces": {
            "Springfield": {
                 "Angle": 45,
                 "Area": 285
            },
            "Lawrence": {
                 "Region": "38A"
            }
        }
    }"#;
    
    #[derive(Debug, Deserialize)]
    struct MyEntry {
        #[serde(rename = "Angle", default)]
        angle: u32,
        #[serde(rename = "Region")]
        region: Option<String>,
        #[serde(rename = "Area", default)]
        area: u32,
    }
    
    #[derive(Debug, Deserialize)]
    struct MyDefaults {
        settings: MyEntry,
    }
    
    #[derive(Debug, Deserialize)]
    struct MyData {
        #[serde(rename = "Defaults")]
        defaults: MyDefaults,
        indinces: HashMap<String, MyEntry>,
    }
    
    fn main() {
        let data: MyData = serde_json::from_str(DATA).expect("failed to parse JSON");
    
        println!("{:#?}", data);
    }
    
    MyData {
        defaults: MyDefaults {
            settings: MyEntry {
                angle: 90,
                region: Some(
                    "27B",
                ),
                area: 187,
            },
        },
        indinces: {
            "Lawrence": MyEntry {
                angle: 0,
                region: Some(
                    "38A",
                ),
                area: 0,
            },
            "Springfield": MyEntry {
                angle: 45,
                region: None,
                area: 285,
            },
        },
    }
    

    To achieve the 'falling back to defaults also specified in the JSON' requirement, you could provide a PartialIndince and an Indince struct that can be converted into each other providing a default:

    use std::{collections::HashMap, sync::Arc};
    
    use serde::Deserialize;
    
    const DATA: &str = r#"{
        "Defaults":{
            "settings": {
                 "Angle": 90,
                 "Region": "27B",
                 "Area": 187
            }
        },
        "indinces": {
            "Springfield": {
                 "Angle": 45,
                 "Area": 285
            },
            "Lawrence": {
                 "Region": "38A"
            }
        }
    }"#;
    
    #[derive(Debug, Deserialize)]
    struct PartialIndince {
        #[serde(rename = "Angle")]
        angle: Option<u32>,
        #[serde(rename = "Region")]
        region: Option<Arc<str>>,
        #[serde(rename = "Area")]
        area: Option<u32>,
    }
    
    #[derive(Debug, Deserialize)]
    struct Indince {
        #[serde(rename = "Angle")]
        angle: u32,
        #[serde(rename = "Region")]
        region: Arc<str>,
        #[serde(rename = "Area")]
        area: u32,
    }
    
    #[derive(Debug, Deserialize)]
    struct MyDefaults {
        settings: Indince,
    }
    
    #[derive(Debug, Deserialize)]
    struct MyData {
        #[serde(rename = "Defaults")]
        defaults: MyDefaults,
        indinces: HashMap<String, PartialIndince>,
    }
    
    impl PartialIndince {
        fn fill_defaults(self, default: &Indince) -> Indince {
            Indince {
                angle: self.angle.unwrap_or(default.angle),
                region: self.region.unwrap_or(Arc::clone(&default.region)),
                area: self.area.unwrap_or(default.area),
            }
        }
    }
    
    fn process() -> HashMap<String, Indince> {
        let data: MyData = serde_json::from_str(DATA).expect("failed to parse JSON");
    
        data.indinces
            .into_iter()
            .map(|indince| (indince.0, indince.1.fill_defaults(&data.defaults.settings)))
            .collect()
    }
    
    fn main() {
        let data = process();
        println!("{:#?}", data);
    }
    
    {
        "Lawrence": Indince {
            angle: 90,
            region: "38A",
            area: 187,
        },
        "Springfield": Indince {
            angle: 45,
            region: "27B",
            area: 285,
        },
    }
    

    Your problem is of course also solvable via direct JSON accesses, but then you need to decide whether you want ajson or serde_json, both simultaneously does not make much sense.

    Here's a raw version with ajson:

    use std::collections::HashMap;
    
    const DATA: &str = r#"{
        "Defaults":{
            "settings": {
                 "Angle": 90,
                 "Region": "27B",
                 "Area": 187
            }
        },
        "indinces": {
            "Springfield": {
                 "Angle": 45,
                 "Area": 285
            },
            "Lawrence": {
                 "Region": "38A"
            }
        }
    }"#;
    
    #[derive(Debug)]
    struct Indince {
        angle: u64,
        region: String,
        area: u64,
    }
    
    fn process() -> HashMap<String, Indince> {
        let default_map = ajson::get(DATA, "Defaults.settings")
            .expect("JSON was not well-formatted")
            .expect("Defaults.Settings does not exist");
        let indince_map = ajson::get(DATA, "indinces")
            .expect("JSON was not well-formatted")
            .expect("indinces does not exist");
    
        let defaults = Indince {
            angle: default_map.get("Angle").unwrap().unwrap().as_u64().unwrap(),
            region: default_map
                .get("Region")
                .unwrap()
                .unwrap()
                .as_str()
                .unwrap()
                .to_string(),
            area: default_map.get("Area").unwrap().unwrap().as_u64().unwrap(),
        };
    
        let mut return_hash: HashMap<String, Indince> = HashMap::new();
        for (key, value) in indince_map
            .as_object()
            .expect("indinces is not a key-value map")
        {
            return_hash.insert(
                key.to_string(),
                Indince {
                    angle: value
                        .get("Angle")
                        .unwrap()
                        .map(|angle| angle.as_u64().unwrap())
                        .unwrap_or(defaults.angle),
                    region: value
                        .get("Region")
                        .unwrap()
                        .map(|region| region.as_str().unwrap().to_string())
                        .unwrap_or(defaults.region.clone()),
                    area: value
                        .get("Area")
                        .unwrap()
                        .map(|area| area.as_u64().unwrap())
                        .unwrap_or(defaults.area),
                },
            );
        }
    
        return_hash
    }
    
    fn main() {
        let data = process();
        println!("{:#?}", data);
    }
    
    {
        "Lawrence": Indince {
            angle: 90,
            region: "38A",
            area: 187,
        },
        "Springfield": Indince {
            angle: 45,
            region: "27B",
            area: 285,
        },
    }
    

    While this works, it's much more error prone. It also requires a ton of error handling (see all the .unwrap()s in the code which in a real project probably need to be replaced with some actual error handling). But it's possible, of course.