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?
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);
}
For convenience you can add an AsRef
impl of for Host
to InnerHost
or a method to extract it from the enum.