Search code examples
rustserdeserde-json

Can you deserialize a struct from a map or a string?


Consider this Config struct which contains a vector of Host structs:

use serde::Deserialize;
use std::net::IpAddr;

#[derive(Debug, Deserialize)]
struct Config {
    name: String,
    hosts: Vec<Host>
}

#[derive(Debug, Deserialize)]
struct Host {
    addr: IpAddr,
    user: String,
}

Using the derived Deserialize implementation, the following JSON and YAML config files can be deserialized successfully with serde_json and serde_yaml:

{
  "name": "example",
  "hosts": [
    { "addr": "1.1.1.1", "user": "alice" },
    { "addr": "2.2.2.2", "user": "bob" }
  ]
}
---
name: example
hosts:
  - addr: 1.1.1.1
    user: alice
  - addr: 2.2.2.2
    user: bob

However, I would like to also be able to deserialize the Host struct from a string. But, it's important that I can also deserialize it from a map, and ideally the vector could be composed of both formats. For example:

{
  "name": "example",
  "hosts": [
    "[email protected]",
    { "addr": "2.2.2.2", "user": "bob" }
  ]
}
---
name: example
hosts:
  - [email protected]
  - addr: 2.2.2.2
    user: bob

With #[serde(try_from = "String")] on top of the Host struct, I can easily support the string deserialization... but then it doesn't deserialize the map format anymore.

The serde website has a page about deserializing either a string or a struct, but it requires the deserialize_with attribute which can only be applied to a field (not to a struct container). I'm not sure this technique would work as my field is a Vec<Host> and not just a Host.

Is this possible to achieve?


Solution

  • You can use an untagged enum for that. Combined with a custom deserializer:

    use std::str::FromStr;
    use serde::{Deserialize, Deserializer};
    use std::net::IpAddr;
    
    #[derive(Debug, Deserialize)]
    struct Config {
        name: String,
        hosts: Vec<Host>,
    }
    
    #[derive(Debug, Deserialize)]
    struct InnerHost {
        addr: IpAddr,
        user: String,
    }
    
    #[derive(Debug, Deserialize)]
    #[serde(untagged)]
    enum Host {
        #[serde(deserialize_with = "deserialize_host_from_str")]
        FromStr(InnerHost),
        FromDict(InnerHost),
    }
    
    fn deserialize_host_from_str<'de, D>(deserializer: D) -> Result<InnerHost, D::Error>
    where
        D: Deserializer<'de>,
    {
        let value = String::deserialize(deserializer)?;
        // parse the value and return host
        Ok(InnerHost {
            addr: IpAddr::from_str("1.1.1.1").unwrap(),
            user: "foobar".to_string(),
        })
    }
    
    fn main() {
        let data = r#"{
      "name": "example",
      "hosts": [
        "[email protected]",
        { "addr": "2.2.2.2", "user": "bob" }
      ]
    }"#;
    
        let config : Config = serde_json::from_str(data).unwrap();
        println!("{:?}", config);
    }
    

    Playground

    For convenience you can add an AsRef impl of for Host to InnerHost or a method to extract it from the enum.