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