'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 1:[1]

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.

Solution 2:[2]

Here is an even cleaner solution that does not need to expose a wrapper type. Modified from here.

use serde::{Deserialize, Deserializer};
use std::net::IpAddr;
use std::str::FromStr;

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

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

impl<'de> Deserialize<'de> for Host {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        #[derive(Deserialize)]
        #[serde(remote = "Host")] // cannot use `Self` here
        struct This {
            addr: IpAddr,
            user: String,
        }

        #[derive(Deserialize)]
        #[serde(untagged)]
        enum Helper {
            Short(String),
            #[serde(with = "This")]
            Full(Host),
        }

        Ok(match Helper::deserialize(deserializer)? {
            Helper::Short(value) => {
                let _ = value; // parse value here
                Self {
                    addr: IpAddr::from_str("1.1.1.1").unwrap(),
                    user: "foobar".to_string(),
                }
            }
            Helper::Full(this) => this,
        })
    }
}

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);
}

Rust Playground

All deserialization logic is done within the Host type itself without any convention for its caller (I mean Config type here).

The key idea is using remote attribute to let the default deserialize function be generated in another namespace.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Netwave
Solution 2 QuarticCat