How to "deserialize with" for a container using serde in Rust
Asked Answered
O

1

8

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
}
Outfit answered 19/2, 2019 at 8:30 Comment(0)
S
6

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.

Solute answered 19/2, 2019 at 10:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.