Deserializing into a DateTime<Utc> and Option<DateTime<Utc>>

Hi all, I'm writing some code that interacts with a web API. The timestamps provided by the API are written in a custom format (unix.microseconds), so I wrote a custom Serializer/Deserializer:

pub mod unix_timestamp {
    use chrono::{DateTime, NaiveDateTime, Utc};
    use serde::{self, Deserialize, Deserializer, Serializer};

    const FORMAT: &'static str = "%s.%6f";

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

    pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        let dt = NaiveDateTime::parse_from_str(&s, FORMAT).map_err(serde::de::Error::custom)?;
        Ok(DateTime::<Utc>::from_naive_utc_and_offset(dt, Utc))
    }
}

And Serialize/Deserialize using the #[serde(with="")] option.

My question is: if there is an optional field in the web API, I cannot use this same mod so far if the value is Option<>. So for example, if the payload can either be:

{"field1": "hello", "timestamp1": "1714521961.793749"}

or

{"field1": "hello", "field2": "bye", "timestamp1": "1714521961.793749", "timestamp2": "1714521961.793749"}

I can do:

#[derive(Serialize, Deserialize)]
pub struct Payload {
    field1: String
    field2: Option<String>
     ...

for the field parts, but I cannot do

    #[serde(with="unix_timestamp")]
    timestamp1: DateTime<Utc>
    #[serde(with="unix_timestamp")]
    timestamp2: Option<DateTime<Utc>>
}

It tells me that: expected Option<DateTime<Utc>>, found DateTime<Utc>

I can get around this by adding another mod, unix_timestamp_opt, with the different return type, but is there a way to re use the same code for both?

This seems to be a long-standing requested feature.

So, the answer is I must write two mods?

Yes. Or you could create a new type and use it instead of DateTime<Utc>. Here's an example.

1 Like

The serde_with crate may also help, as its plumbing is typically a bit more composable than #[serde(with)] directly.

2 Likes

serde_with comes with all the stuff to deserialize your format. Serialization works too, but will only use 6 digits past the decimal if required.

#[serde_with::serde_as]
#[derive(Debug, Serialize, Deserialize)]
pub struct Payload {
    field1: String,
    field2: Option<String>,
    #[serde_as(as = "serde_with::TimestampSecondsWithFrac<String>")]
    timestamp1: DateTime<Utc>,
    #[serde_as(as = "Option<serde_with::TimestampSecondsWithFrac<String>>")]
    timestamp2: Option<DateTime<Utc>>,
}

Cargo.toml

serde_with = { version = "3.8.1", features = ["chrono_0_4"] }
3 Likes

Just write a newtype wrapper around DateTime. IMO the whole serde(with) thing is an anti-pattern, precisely because it doesn't compose (as you observed).

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.