Search code examples
rusttraitsserde

How can I implement serde for a type that I don't own and have it support compound /wrapper/collection types


This question is similar How do I implement a trait I don't own for a type I don't own?

I wrote a serializer for Date, using the mechanism described in the documentation with my module wrapping a serialize function


pub mod my_date_format {
    use chrono::{Date, NaiveDate, Utc};
    use serde::{self, Deserialize, Deserializer, Serializer};

    const SERIALIZE_FORMAT: &'static str = "%Y-%m-%d";

    pub fn serialize<S>(date: &Date<Utc>, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let s = format!("{}", date.format(SERIALIZE_FORMAT));
        serializer.serialize_str(&s)
    }

    pub fn deserialize<'de, D>(deserializer: D) -> Result<Date<Utc>, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        NaiveDate::parse_from_str(s.as_str(), SERIALIZE_FORMAT)
            .map_err(serde::de::Error::custom)
            .map(|x| {
                let now = Utc::now();
                let date: Date<Utc> = Date::from_utc(x, now.offset().clone());
                date
            })
    }
}

then I can do:

struct MyStruct {
    #[serde(with = "my_date_format")]
    pub start: Date<Utc>,
}

Problem is if I wrap the serialized thing in other types (which are serializable themselves) I get errors:

#[serde(with = "my_date_format")]
pub dates: Vec<Date<Utc> // this won't work now since my function doesn't serialize vectors
pub maybe_date: Option<Date<Utc>>> // won't work
pub box_date: Box<Date<Utc>> // won't work...

How can I gain the implementations provided while using my own serializer?

https://docs.serde.rs/serde/ser/index.html#implementations-of-serialize-provided-by-serde


Solution

  • Instead of relying on wrapper types it is possible to achieve the same results with the serde_as macro from the serde_with crate. It works like the serde with attribute but also supports wrapper and collections types.

    Since you already have a module to use with serde's with, the hard part is already done. You can find the details in the crate documentation. You only need to add a local type and two boilerplate implementations for the traits SerializeAs and DeserializeAs to use your custom transformations.

    use chrono::{Date, NaiveDate, Utc};
    
    struct MyDateFormat;
    
    impl serde_with::SerializeAs<Date<Utc>> for MyDateFormat {
        fn serialize_as<S>(value: &Date<Utc>, serializer: S) -> Result<S::Ok, S::Error>
        where
            S: serde::Serializer,
        {  
            my_date_format::serialize(value, serializer)
        }
    }
    
    impl<'de> serde_with::DeserializeAs<'de, Date<Utc>> for MyDateFormat {
        fn deserialize_as<D>(deserializer: D) -> Result<Date<Utc>, D::Error>
        where
            D: serde::Deserializer<'de>,
        {  
            my_date_format::deserialize(deserializer)
        }
    }
    
    #[serde_with::serde_as]
    #[derive(Serialize, Deserialize, Debug)]
    struct MyStruct {
        #[serde_as(as = "MyDateFormat")]
        date: Date<Utc>,
        #[serde_as(as = "Vec<MyDateFormat>")]
        dates: Vec<Date<Utc>>,
        #[serde_as(as = "Option<MyDateFormat>")]
        opt_date: Option<Date<Utc>>,
        #[serde_as(as = "Box<MyDateFormat>")]
        boxed_date: Box<Date<Utc>>,
    }
    
    fn main() {
        let s = MyStruct {
            date: Utc::now().date().into(),
            dates: std::iter::repeat(Utc::now().date().into()).take(4).collect(),
            opt_date: Some(Utc::now().date().into()),
            boxed_date: Box::new(Utc::now().date().into()),
        };
    
        let json = serde_json::to_string_pretty(&s).unwrap();
        println!("{}", json);
    }
    
    // This module is taken uunmodified from the question
    pub mod my_date_format {
        use chrono::{Date, NaiveDate, Utc};
        use serde::{self, Deserialize, Deserializer, Serializer};
    
        const SERIALIZE_FORMAT: &'static str = "%Y-%m-%d";
    
        pub fn serialize<S>(date: &Date<Utc>, serializer: S) -> Result<S::Ok, S::Error>
        where
            S: Serializer,
        {
            let s = format!("{}", date.format(SERIALIZE_FORMAT));
            serializer.serialize_str(&s)
        }
    
        pub fn deserialize<'de, D>(deserializer: D) -> Result<Date<Utc>, D::Error>
        where
            D: Deserializer<'de>,
        {
            let s = String::deserialize(deserializer)?;
            NaiveDate::parse_from_str(s.as_str(), SERIALIZE_FORMAT)
                .map_err(serde::de::Error::custom)
                .map(|x| {
                    let now = Utc::now();
                    let date: Date<Utc> = Date::from_utc(x, now.offset().clone());
                    date
                })
        }
    }