MVCE:
use std::collections::HashMap;
use std::fmt;
use std::marker::PhantomData;
use std::str::FromStr;
use serde; // 1.0.85
use serde::de::{self, MapAccess, Visitor}; // 1.0.85
use serde_derive::Deserialize; // 1.0.85
use toml; // 0.4.10
use void::Void; // 1.0.2
// See: https://serde.rs/string-or-struct.html
fn string_or_struct<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
T: serde::Deserialize<'de> + FromStr<Err = Void>,
D: serde::Deserializer<'de>,
{
// This is a Visitor that forwards string types to T's `FromStr` impl and
// forwards map types to T's `Deserialize` impl. The `PhantomData` is to
// keep the compiler from complaining about T being an unused generic type
// parameter. We need T in order to know the Value type for the Visitor
// impl.
struct StringOrStruct<T>(PhantomData<fn() -> T>);
impl<'de, T> Visitor<'de> for StringOrStruct<T>
where
T: serde::Deserialize<'de> + FromStr<Err = Void>,
{
type Value = T;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("string or map")
}
fn visit_str<E>(self, value: &str) -> Result<T, E>
where
E: de::Error,
{
Ok(FromStr::from_str(value).unwrap())
}
fn visit_map<M>(self, visitor: M) -> Result<T, M::Error>
where
M: MapAccess<'de>,
{
// `MapAccessDeserializer` is a wrapper that turns a `MapAccess`
// into a `Deserializer`, allowing it to be used as the input to T's
// `Deserialize` implementation. T then deserializes itself using
// the entries from the map visitor.
serde::Deserialize::deserialize(de::value::MapAccessDeserializer::new(visitor))
}
}
deserializer.deserialize_any(StringOrStruct(PhantomData))
}
impl FromStr for Obj {
type Err = Void;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Obj {
x: 0,
y: s.to_owned(),
})
}
}
// ----------------------------------------------------------------------------
#[derive(Debug, Deserialize)]
struct Obj {
x: isize,
y: String,
}
#[derive(Debug, Deserialize)]
struct Simple {
#[serde(deserialize_with = "string_or_struct")]
obj: Obj,
}
#[derive(Debug, Deserialize)]
struct InsideHashMap {
objs: HashMap<String, Obj>,
}
fn main() {
// Basic deserialization of Obj
let toml = r#"
x = 5
y = "hello"
"#;
let obj: Obj = toml::from_str(toml).unwrap();
println!("{:?}", obj);
// Basic deserialization of Obj as a field in a struct
let toml = r#"
[obj]
x = 5
y = "hello"
"#;
let simple: Simple = toml::from_str(toml).unwrap();
println!("{:?}", simple);
// Basic deserialization of Obj as a field in a struct as a string or struct
let toml = r#"
obj = "hello"
"#;
let simple: Simple = toml::from_str(toml).unwrap();
println!("{:?}", simple);
// Deserialization of an Obj inside a HashMap
let toml = r#"
[objs]
a = { x = 5, y = "hello" }
"#;
let working: InsideHashMap = toml::from_str(toml).unwrap();
println!("{:?}", working);
// Deserialization of Obj inside a HashMap field as a string or struct
let toml = r#"
[objs]
a = "hello"
"#;
let not_working: InsideHashMap = toml::from_str(toml).unwrap();
println!("{:?}", not_working);
}
I want to use serde to deserialize a TOML format where a struct can be specified as string or the normal struct specification
a = "a string"
b = { x = 5, y = "another string" }
In this example I would end up with a HashMap that looks like
{
"a": Obj { x: 0, y: "a string" },
"b": Obj { x: 5, y: "another string" }
}
I have read https://serde.rs/string-or-struct.html on how to use the "deserialize_with" attribute on a struct field. But how do I go about this when the struct is inside a container like a HashMap?
#[derive(Debug, Deserialize)]
struct Obj {
x: isize,
y: String
}
#[derive(Debug, Deserialize)]
struct Simple {
#[serde(deserialize_with = "string_or_struct")]
obj: Obj
}
#[derive(Debug, Deserialize)]
struct InsideHashMap {
objs: HashMap<String, Obj> // <-- how can I use "deserialize_with" on Obj here
}
Serde is a framework for ser ializing and de serializing Rust data structures efficiently and generically. The Serde ecosystem consists of data structures that know how to serialize and deserialize themselves along with data formats that know how to serialize and deserialize other things.
In most cases Serde's derive is able to generate an appropriate implementation of Deserialize for structs and enums defined in your crate. Should you need to customize the deserialization behavior for a type in a way that derive does not support, you can implement Deserialize yourself.
A data structure that can be deserialized from any data format supported by Serde, analogue to Deserialize. A data structure that can be serialized into any data format supported by Serde, analogue to Serialize.
Implementing Deserialize for a type tends to be more complicated than implementing Serialize. The Deserializer trait supports two entry point styles which enables different kinds of deserialization. The deserialize_any method.
First, we need another struct to use deserialize_with
for our HashMap:
#[derive(Debug, Deserialize)]
struct Flatten {
#[serde(deserialize_with = "string_or_struct", flatten)]
obj: Obj,
}
So we can write:
#[derive(Debug, Deserialize)]
struct InsideHashMap {
objs: HashMap<String, Flatten>,
}
This should work but it's not because (I don't really know why yet, look like flatten and deserialize_with
doesn't work together, it's seem that it doesn't use deserialize_with
implementation)
So, we must use the hard way, let implement it:
use std::collections::HashMap;
use std::fmt;
use std::str::FromStr;
use serde; // 1.0.85
use serde::de::{self, Deserialize, MapAccess, Visitor}; // 1.0.85
use serde::Deserializer;
use serde_derive::Deserialize; // 1.0.85
use toml; // 0.4.10
use void::Void; // 1.0.2
#[derive(Debug)]
struct Obj {
x: isize,
y: String,
}
struct ObjVisitor;
// OjbAux is here to avoid implement the deserialiser of the map by hand we can't use
// Obj cause it will cause infinite recursion
#[derive(Debug, Deserialize)]
struct ObjAux {
x: isize,
y: String,
}
impl<'de> Visitor<'de> for ObjVisitor {
type Value = Obj;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("string or map")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(FromStr::from_str(value).unwrap())
}
fn visit_map<M>(self, visitor: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let aux: ObjAux = Deserialize::deserialize(de::value::MapAccessDeserializer::new(visitor))?;
Ok(Obj { x: aux.x, y: aux.y })
}
}
impl<'de> Deserialize<'de> for Obj {
fn deserialize<D>(deserializer: D) -> Result<Obj, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(ObjVisitor)
}
}
impl FromStr for Obj {
type Err = Void;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Obj {
x: 0,
y: s.to_owned(),
})
}
}
#[derive(Debug, Deserialize)]
struct Simple {
obj: Obj,
}
#[derive(Debug, Deserialize)]
struct InsideHashMap {
objs: HashMap<String, Obj>,
}
fn main() {
// Basic deserialization of Obj
let toml = r#"
x = 5
y = "hello"
"#;
let obj: Obj = toml::from_str(toml).unwrap();
println!("{:?}", obj);
// Basic deserialization of Obj as a field in a struct
let toml = r#"
[obj]
x = 5
y = "hello"
"#;
let simple: Simple = toml::from_str(toml).unwrap();
println!("{:?}", simple);
// Basic deserialization of Obj as a field in a struct as a string or struct
let toml = r#"
obj = "hello"
"#;
let simple: Simple = toml::from_str(toml).unwrap();
println!("{:?}", simple);
// Deserialization of an Obj inside a HashMap
let toml = r#"
[objs]
a = { x = 5, y = "hello" }
"#;
let working: InsideHashMap = toml::from_str(toml).unwrap();
println!("{:?}", working);
// Deserialization of Obj inside a HashMap field as a string or struct
let toml = r#"
[objs]
a = "hello"
"#;
let not_working: InsideHashMap = toml::from_str(toml).unwrap();
println!("{:?}", not_working);
}
This work as expected.
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