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!