`serde` custom serialization for generic type

I am unsure how to implement the following design pattern in the best way.

Background

I have a HashMap type defined as

type MyMap<T> = HashMap<String, Option<T>>

Using serde, when serializing I want to only serialize the map's keys as an array, and when deserializing from an array of keys I want to initialize all values to None.

Examples

Serialization (MyMap<i32>)

{
    "k1": None,
    "k2": Some(0)
}

should serialize to

["k1", "k2"]

Deserialization (MyMap<i32>)

["k1", "k2"]

should deserialize to

{
    "k1": None,
    "k2": None
}

I would like to accomplish this using serde's serialize_with and deserialize_with field attributes.

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct MyStruct {
    #[serde(serialize_with = "serialize_my_map")]
    #[serde(deserialize_with = "deserialize_my_map")]
    i32_map: MyMap<i32>,
    
    #[serde(serialize_with = "serialize_my_map")]
    #[serde(deserialize_with = "deserialize_my_map")]
    f64_map: MyMap<f64>,
}

Attempt

I tried to create a serialize_my_map and deserialize_my_map functions to accomplish this.

use std::fmt;
use serde::de;
use serde::ser::{SerializeSeq, Serializer};

fn serialize_my_map<S, T>(
    map: &MyMap<T>,
    serializer: S,
) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    let mut seq = serializer.serialize_seq(Some(map.len()))?;
    for (k, _) in map.iter() {
        seq.serialize_element(&k)?;
    }

    seq.end()
}

fn deserialize_my_map<'de, D, T>(
    deserializer: D,
) -> Result<MyMap<T>, D::Error>
where
    D: de::Deserializer<'de>,
{
    struct ElmVisitor;

    impl<'de> de::Visitor<'de> for ElmVisitor {
        type Value = T;

        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
            formatter.write_str("")
        }

        fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
        where
            A: de::SeqAccess<'de>,
        {
            let mut map: MyMap<T>;
            match seq.size_hint() {
                None => map = MyMap::new(),
                Some(s) => map = MyMap::with_capacity(s),
            };

            while let Some(key) = seq.next_element::<T>()? {
                map.insert(key, None);
            }

            Ok(map)
        }
    }

    deserializer.deserialize_seq(ElmVisitor)
}

However, this won't work for several reasons.

  1. The function signatures of serialize_my_map and deserialize_my_map do not match those required by serde's serialize_with and deserailize_with attributes.
  2. In deserialize_my_map the outer generic type T is being used inside the implementation of of de::Visitor for ElmVisitor.

Any help on how to accomplish this would be greatly appreciated.
Rust Playground: Rust Playground

They do.

Then why don't you just move it to the outside?


Anyway, there were other problems with your code; I improved it somewhat by doing the following:

  • I moved the de/serialization logic to its own module, so you can use #[serde(with = "...")] instead of the repetitive serialize_with and deserialize_with attributes.
  • I removed the uninitialized variable which is assigned to in a match and replaced it with unwrap_or(). You could do this with match too, as it's also an expression.
  • I corrected the type Value = T; associated type to say type Value = MyMap<T>;
  • I rewrote for (k, _) in map.iter() as for k in map.keys() for the sake of clarity.
  • I replaced the key serialization logic with collect_seq().

See the completed code.


One more thing that you should consider is creating a newtype wrapper that itself implements (De)Serialize, so that users of your typedef don't have to remember to add the #[serde(with)] attribute all the time. This approach is demonstrated in this Playground.

1 Like

I found a couple of problems while fixing the code:

  1. The ElmVisitor needs its own generic parameter. You can make it a `ElmVisitor(PhantomData).
  2. The type Value for the ElmVisitor is wrong. You want the Visitor to produce a value of type MyMap<T>.
  3. The elements you are deserializing from the list (next_element::<T>) are not Ts but rather Strings. The T is in the value part of the map which you are setting to None.

There are also two cleanups:

  1. Collect the functions in a module, such that a single #[serde(with = "...")] attribute works.
  2. You can simplify the serialization part using collect_seq.
1 Like

Thank you for all the help. I was able to implement it. I also used the Implement Deserialize for a custom map type provided by serde for guidance.