您可以反序列化映射或字符串中的结构吗?

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

考虑这个 Config 结构,它包含一个 Host 结构的向量:

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,
}

使用派生的 Deserialize 实现,可以使用 serde_jsonserde_yaml 成功反序列化以下 JSON 和 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

但是,我还希望能够从字符串中反序列化 Host 结构。但是,重要的是我还可以从地图中反序列化它,理想情况下矢量可以由两种格式组成。例如:

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

Host 结构之上使用 #[serde(try_from = "String")],我可以轻松支持字符串反序列化...但是它不再反序列化地图格式了。

serde 网站有一个关于反序列化字符串或结构的 page,但它需要 deserialize_with 属性,该属性只能应用于字段(不适用于结构容器)。我不确定这种技术是否有效,因为我的领域是 Vec<Host> 而不仅仅是 Host.

这可以实现吗?

您可以为此使用未标记的枚举。结合自定义反序列化器:

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": [
    "alice@1.1.1.1",
    { "addr": "2.2.2.2", "user": "bob" }
  ]
}"#;

    let config : Config = serde_json::from_str(data).unwrap();
    println!("{:?}", config);
}

Playground

为方便起见,您可以将 HostAsRef 实现添加到 InnerHost 或从枚举中提取它的方法。

这是一个更简洁的解决方案,不需要公开包装器类型。修改自 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": [
        "alice@1.1.1.1",
        { "addr": "2.2.2.2", "user": "bob" }
      ]
    }"#;

    let config: Config = serde_json::from_str(data).unwrap();
    println!("{:?}", config);
}

Rust Playground

所有反序列化逻辑都在 Host 类型本身内完成,对其调用者没有任何约定(我指的是 Config 类型)。

关键思想是使用 remote 属性让默认的反序列化函数在另一个命名空间中生成。