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);
}
Playground
For convenience you can add an AsRef impl of for Host to InnerHost or a method to extract it from the enum.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With