Serializing a chrono::DateTime Using Serde
Have you ever found yourself in need of formatting dates created with chrono for your users? If so, I would like to share a solution I used at work recently.
In an attempt to build a report for my business line, I was fetching data from my Postgresql DB using sqlx which would end up in a tsv file. However, I had multiple reports that required the same data from the same query but they required different formatting for their dates.
We use chrono::DateTime<Utc> to map SQL's timestamptz type to Rust.
First, we have a table with an optional timestamptz field.
create table users (
name varchar not null,
updated_at timestamptz
)
We then have our corresponding struct with matching fields. In this case, the DateTime is optional.
pub struct User {
name: String,
#[serde(serialize_with = "serialize_dt")]
updated_at: Option<chrono::DateTime<Utc>>,
}
Finally, we implement our custom serializer 'serialize_dt', which serde will use whenever serializing the updated_at field on User. But first, lets look at the requirements. Serde's documentation informs us of the following:
#[serde(serialize_with = "path")]
Serialize this field using a function that is different from its implementation of
Serialize
. The given function must be callable asfn<S>(&T, S) -> Result<S::Ok, S::Error> where S: Serializer
, although it may also be generic overT
. Fields used withserialize_with
are not required to implementSerialize
.
This is what the function signature will look like:
pub fn serialize_dt<S>(dt: &Option<DateTime<Utc>>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
todo!()
}
Next, because our DateTime is wrapped in an Option, we use an 'if let Some()' to handle both its Some and None variants.
pub fn serialize_dt<S>(dt: &Option<DateTime<Utc>>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if let Some(dt) = dt {
todo!()
} else {
todo!()
}
}
In case we do have a DateTime, we want to format its output. For that, we can call .format() on it, which expects a &str representing a valid escape sequence [See link for supported sequences]. The .format() method returns a DelayedFormat<StrftimeItems<'a>>, which doesn't implement the .serialize() method. To remedy that, we will simply turn it into a String using .to_string() chain call .serialize() and pass to it our serializer.
pub fn serialize_dt<S>(dt: &Option<DateTime<Utc>>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if let Some(dt) = dt {
dt.format("%m/%d/%Y %H:%M")
.to_string()
.serialize(serializer)
} else {
todo!()
}
}
Finally, all we have to do is handle the None variant of our Option. Which is made simple by the Serializer implementation. We simply use the .serialize_none() method on serializer.
pub fn serialize_dt<S>(dt: &Option<DateTime<Utc>>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if let Some(dt) = dt {
dt.format("%m/%d/%Y %H:%M")
.to_string()
.serialize(serializer)
} else {
serializer.serialize_none()
}
}
Here you go! We now have a human-readable date for our report, which we can apply to any field using Option<chrono::DateTime<Utc>>. We could have multiple variants for multiple formats, but by now you should be good to go to build your own. I hope this will have helped you come to a nice solution faster than me.
Cheers!
Subscribe to my newsletter
Read articles from Kirk Paradis directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Kirk Paradis
Kirk Paradis
I am a Canadian Software Engineer living in Japan. My goal is to wake up every morning being a better engineer than I was yesterday.