rustdeserializationserde

Deserialize two different types into the same one


I have a simple structure that contains a resources field. I would like my resources to always be Urls. In the actual file that I am trying to deserialize, resources can either be URLs or Path.

Here is my structure:

pub struct Myfile {
    pub resources: Vec<Resource>,
}

pub type Resource = Url;

I would like to use serde to:

  1. Try to deserialize each Resource using the implementation from the url crate.
  2. If it fails, try to deserialize each one of them into a Path and then use url::from_*_path() to get a Url.

I am trying to adapt the string or map,map and structure examples but I am struggling to understand where to even start.

Since my end result will by a Url, the examples seem to show that I should be implementing Deserialize for Url. But I still need to current implementation. My Resource is an alias so I can't implement Deserialize for it.

Is there any simple way to deserialize both Paths and Urls into Urls?


Solution

  • I think the key to doing this with reasonable effort is to realize that serde::Deserialize for Url is also just cooking with water, i.e. just expecting a string and calling Url::parse on it.

    So it's time to deploy my favourite serde trick: deserialize to a struct that serde can handle easily:

    #[derive(Deserialize)]
    pub struct MyfileDeserialize {
        pub resources: Vec<String>,
    }
    

    Tell serde that it should get the struct you finally want from that easily handlable struct:

    #[derive(Deserialize, Debug)]
    #[serde(try_from = "MyfileDeserialize")]
    pub struct Myfile {
        pub resources: Vec<Resource>,
    }
    

    Finally, you just need to define how to turn MyfileDeserialize into Myfile.

    impl TryFrom<MyfileDeserialize> for Myfile {
        type Error = &'static str; // TODO: replace with anyhow or a proper error type
        fn try_from(t: MyfileDeserialize) -> Result<Self, &'static str> {
            Ok(Self {
                resources: t
                    .resources
                    .into_iter()
                    .map(|url| {
                        if let Ok(url) = Url::parse(&url) {
                            return Ok(url);
                        };
                        if let Ok(url) = Url::from_file_path(url) {
                            return Ok(url);
                        };
                        // try more from_*_path here.
                        Err("Can't as url or path")
                    })
                    .collect::<Result<Vec<_>, _>>()?,
            })
        }
    }
    

    Playground


    Edit regarding your PS:

    If you are willing to mess around with manually implementing deserializer traits and functions, I suppose you do have the option of completely getting rid of any wrapper structs or mediating TryFrom types: add a #[serde(deserialize_with = …)] to your resources, and in there, first do a <Vec<String>>::deserialize(de)?, and then turn that into a Vec<Url> as usual.

    Playground