jsonrustdeserializationflattenserde

How do I deserialize JSON in Rust when combining a flattened component with the need to convert keys?


I apologize for somewhat unclear question, but I can't really phrase it well with one sentence. Below is a toy example illustrating the problem:

use serde_json;
use serde_json::Value;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

#[derive(Deserialize, Serialize)]
struct RawInfo {
    info: HashMap<u32,String>
}

#[derive(Deserialize, Serialize)]
struct InfoContainer {
    info: HashMap<u32,String>
}

#[derive(Deserialize, Serialize)]
struct ContainedInfo {
    #[serde(flatten)]
    container: InfoContainer
}

#[derive(Deserialize, Serialize)]
struct InfoContainer2 {
    info: HashMap<String,String>
}

#[derive(Deserialize, Serialize)]
struct ContainedInfo2 {
    #[serde(flatten)]
    container: InfoContainer2
}

fn main() {
    let jstr = r#"
{
    "info": {
        "1": "first",
        "2": "second",
        "3": "third"
    }
}
    "#;
    
    let jv: Value = match serde_json::from_str(jstr) {
        Ok(jv) => jv,
        Err(jv) => {
            println!("serde json error: {}", jv);
            panic!();
        }
    };
    
    
    let raw_deserialized = match <RawInfo as Deserialize>::deserialize(jv.clone()) {
        Ok(raw_deserialized) => println!("raw is ok"),
        Err(raw_deserialized) => println!("raw is error: {}", raw_deserialized)
    };
    
    let container_deserialized = match <ContainedInfo as Deserialize>::deserialize(jv.clone()) {
        Ok(container_deserialized) => println!("container is ok"),
        Err(container_deserialized) => println!("container is error: {}", container_deserialized)
    };

    let container_deserialized2 = match <ContainedInfo2 as Deserialize>::deserialize(jv.clone()) {
        Ok(container_deserialized2) => println!("container2 is ok"),
        Err(container_deserialized2) => println!("container2 is error: {}", container_deserialized2)
    };
    
}

This results in the following output:

raw is ok
container is error: invalid type: string "1", expected u32
container2 is ok

So when the Rust struct has the same structure as the JSON, the deserialization conversion of keys works fine, but we attempt to reconfigure it with flatten, it fails because the target HashMap has integer keys while the source has string keys.

JSON keys must be strings, so I can't change that, and I need to have integer keys in my HashMap store. I would like to also have the flattening, though. Can I achieve that without needing to write a custom deserializer?


Solution

  • This is very interesting. serde_json has a hack to support deserializing maps with integer keys, but this hack doesn't work with #[serde(flatten)] because the type doesn't say it expects a map.

    Fortunately, as with many things with serde, serde_with comes to your help:

    use std::collections::HashMap;
    
    use serde::{Deserialize, Serialize};
    
    #[serde_with::serde_as]
    #[derive(Debug, Deserialize, Serialize)]
    struct InfoContainer {
        #[serde_as(as = "HashMap<serde_with::DisplayFromStr, _>")]
        info: HashMap<u32, String>,
    }
    
    #[derive(Debug, Deserialize, Serialize)]
    struct ContainedInfo {
        #[serde(flatten)]
        container: InfoContainer,
    }
    
    fn main() {
        let jstr = r#"
    {
        "info": {
            "1": "first",
            "2": "second",
            "3": "third"
        }
    }
        "#;
    
        let container_deserialized = serde_json::from_str::<ContainedInfo>(jstr).unwrap();
        dbg!(container_deserialized);
    }