skip to Main Content

I have the following json file that I want to deserialize in a Rust struct:

{
  "name": "Manuel",
  "friends": {
    "id1": 1703692376,
    ...
  } 
  ...
}

The friends tag contains the ids and a timestamp
I created a Rust struct like this:

use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug,Clone,Serialize,Deserialize)]
pub struct MyStruct{
    name: String,
    friends: HashMap<String, i64>
}

And that works fine, but I would actually like to have something like this:

use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug,Clone,Serialize,Deserialize)]
pub struct MyStruct{
    name: String,
    friends: HashMap<String, SystemTime>
}

So I would like to transform the integer value to a SystemTime type. Is it possible to do that with serde?

2

Answers


  1. First, lets create conversion functions unix_seconds_to_system and system_to_unix_seconds to go between i64 and SystemTime.

    fn unix_seconds_to_system(unix_seconds: i64) -> SystemTime {
        if unix_seconds.is_positive() {
            let d = Duration::from_secs(unix_seconds as u64);
            SystemTime::UNIX_EPOCH + d
        } else {
            let d = Duration::from_secs(-unix_seconds as u64);
            SystemTime::UNIX_EPOCH - d
        }
    }
    
    fn system_to_unix_seconds(system: SystemTime) -> i64 {
        match system.duration_since(SystemTime::UNIX_EPOCH) {
            Ok(t) => t.as_secs() as i64,
            Err(e) => -(e.duration().as_secs() as i64),
        }
    }
    

    Then, you can use #[serde(with)] to specify custom functions to use for deserialization and serialization instead of the Deserialize and Serialize implementations for HashMap<String, SystemTime>.

    #[derive(Debug, Clone, Serialize, Deserialize)]
    pub struct MyStruct {
        name: String,
        #[serde(with = "time_map")]
        friends: HashMap<String, SystemTime>,
    }
    
    mod time_map {
        use super::*;
        use serde::{Deserializer, Serializer};
    
        pub fn serialize<S>(map: &HashMap<String, SystemTime>, ser: S) -> Result<S::Ok, S::Error>
        where
            S: Serializer,
        {
            todo!()
        }
    
        pub fn deserialize<'de, D>(de: D) -> Result<HashMap<String, SystemTime>, D::Error>
        where
            D: Deserializer<'de>,
        {
            todo!()
        }
    }
    

    There are two ways to go about this. The simpler solution is to go through HashMap<String, i64> and converting.

    pub fn serialize<S>(map: &HashMap<String, SystemTime>, ser: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        map.iter()
            .map(|(k, &v)| (k, system_to_unix_seconds(v)))
            .collect::<HashMap<_, _>>()
            // This serializes `HashMap<&String, i64>`, which has the same
            // representation as `HashMap<String, i64>`.
            .serialize(ser)
    }
    
    pub fn deserialize<'de, D>(de: D) -> Result<HashMap<String, SystemTime>, D::Error>
    where
        D: Deserializer<'de>,
    {
        Ok(HashMap::<String, i64>::deserialize(de)?
            .into_iter()
            .map(|(k, v)| (k, unix_seconds_to_system(v)))
            .collect())
    }
    

    The downside is that this allocates two HashMaps when deserializing and one when serializing, instead of one and zero respectively. To avoid that, you can make fully custom functions using Serializer and Deserializer methods directly.

    use serde::de::Visitor;
    use serde::ser::SerializeMap;
    
    pub fn serialize<S>(map: &HashMap<String, SystemTime>, ser: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut ser_map = ser.serialize_map(Some(map.len()))?;
        for (k, &v) in map {
            let unix_seconds = system_to_unix_seconds(v);
            ser_map.serialize_entry(k, &unix_seconds)?;
        }
        ser_map.end()
    }
    
    pub fn deserialize<'de, D>(de: D) -> Result<HashMap<String, SystemTime>, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct VisitTimeMap;
        
        impl<'de> Visitor<'de> for VisitTimeMap {
            type Value = HashMap<String, SystemTime>;
    
            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                write!(formatter, "a map of strings to unix timestamps")
            }
    
            fn visit_map<A>(self, mut de_map: A) -> Result<Self::Value, A::Error>
            where
                A: serde::de::MapAccess<'de>,
            {
                let mut map = HashMap::with_capacity(de_map.size_hint().unwrap_or_default());
    
                while let Some((k, v)) = de_map.next_entry::<String, i64>()? {
                    let system_time = unix_seconds_to_system(v);
                    map.insert(k, system_time);
                }
    
                Ok(map)
            }
        }
    
        de.deserialize_map(VisitTimeMap)
    }
    

    Playground

    Login or Signup to reply.
  2. serde_with has got you covered. It supports de/serialization of SystemTime from various formats, including seconds since epoch, and unlike #[serde(with)], it supports nested types:

    #[serde_with::serde_as]
    #[derive(Debug, Clone, Serialize, Deserialize)]
    pub struct MyStruct {
        name: String,
        #[serde_as(as = "HashMap<_, serde_with::TimestampSeconds>")]
        friends: HashMap<String, SystemTime>,
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search