Search code examples
rustserdehyper

Deserialize map of remote structs using serde_json


I have a use case that requires deserializing JSON into a map of "remote" (defined in another crate) structs. I've had a laughably difficult time with this, so I must be missing something obvious.

The following is essentially the desired end state:

use hyper::Uri;
use serde_json;
use std::collections::HashMap;

fn main() {
    let data = r#"
        {
            "/a": "http://example.com/86f7e437faa5a7fce15d1ddcb9eaeaea377667b8",
            "/b": "http://example.com/e9d71f5ee7c92d6dc9e92ffdad17b8bd49418f98",
            "/c": "http://example.com/84a516841ba77a5b4648de2cd0dfcb30ea46dbb4"
        }"#;

    let map: HashMap<String, Uri> = serde_json::from_str(data).unwrap();

    println!("{:?}", map);
}

which fails because:

the trait bound `Uri: serde::de::Deserialize<'_>` is not satisfied required because of the requirements
on the impl of `serde::de::Deserialize<'_>` for `HashMap<std::string::String, Uri>`

While the serde docs describe a pretty nasty but potentially viable workaround for deriving Deserialize on remote structs, it requires the use of #[serde(with = "LocalStructRedefinition")] on any referencing container type, which does not appear possible when creating a HashMap.

Intuitively this must be a common use case... is there a way to solve this that doesn't involve:

  1. deserializing the data into a HashMap<String, String>
  2. iterate through the map, parsing the values into a new HashMap<String, Uri>

Solution

  • With a mix of Into, deserialize_with and flatten, you can achieve what you want:

    use serde_json;
    use std::collections::HashMap;
    use hyper::Uri;
    use serde::{de::Error, Deserialize, Deserializer};
    
    #[derive(Debug, Deserialize)]
    struct MyUri(#[serde(deserialize_with = "from_uri")] Uri);
    
    #[derive(Debug, Deserialize)]
    struct MyUriMap {
        #[serde(flatten)]
        inner: HashMap<String, MyUri>
    }
    
    impl Into<HashMap<String, Uri>> for MyUriMap {
        fn into(self) -> HashMap<String, Uri> {
            self.inner.into_iter().map(|x| (x.0, x.1.0)).collect()
        }
    }
    
    
    fn from_uri<'de, D>(deserializer: D) -> Result<Uri, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s: &str = Deserialize::deserialize(deserializer)?;
        s.parse().map_err(D::Error::custom)
    }
    
    
    fn main() {
        let data = r#"
            {
                "/a": "http://example.com/86f7e437faa5a7fce15d1ddcb9eaeaea377667b8",
                "/b": "http://example.com/e9d71f5ee7c92d6dc9e92ffdad17b8bd49418f98",
                "/c": "http://example.com/84a516841ba77a5b4648de2cd0dfcb30ea46dbb4"
            }"#;
    
        let map: MyUriMap = serde_json::from_str(data).unwrap();
    
        // let map: HashMap<String, Uri> = map.into();
        // I think to get HashMap<String, Uri> you have to do an iter as seen in the Into implementation
        println!("{:?}", map);
    }
    

    See in Playground

    PS. In my answer, to get HashMap<String, Uri> you have to do an iter as seen in the Into implementation