Parsing custom datetime format in Rust using serde

2021-10-24
Rust

Most of the date and time related crates like time or chrono already implement serde traits making date or datetime parsing quite easy. This applies just if the format is standard, but what happens if the format is not standard? Let’s say we want to parse datetime looking like this 2021-10-24T07:48:26.389646Z. There is no timezone information so I decided to try the PrimitiveDateTime type from time crate. I didn’t consider using chrono at the moment because of the RUSTSEC-2020-0159 problem.

Fortunately, this is also not that difficult to solve, you just need to implement a custom parsing function. Let’s see the example code:

use anyhow::Result;
use log::warn;
use serde:🇩🇪:Deserializer;
use serde::Deserialize;
use time::PrimitiveDateTime;

// ...

#[derive(Default, Debug, Clone, Deserialize)]
pub struct Message {
    pub id: u64,
    #[serde(deserialize_with = "primitive_date_time_from_str")]
    pub sent: Option<PrimitiveDateTime>,
}

fn primitive_date_time_from_str<'de, D: Deserializer<'de>>(d: D) -> Result<Option<PrimitiveDateTime>, D::Error> {
    let s: Option<String> = Deserialize::deserialize(d)?;
    if s.is_none() {
        return Ok(None);
    }

    let format = time::macros::format_description!(
        "[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond]Z"
    );

    match time::PrimitiveDateTime::parse(&s.unwrap(), &format) {
        Ok(o) => Ok(Some(o)),
        Err(err) => {
            warn!("{}", err);
            Ok(None)
        }
    }
}

Using serde deserialize_with field attribute, we can define which function to call in order to parse this field. In our case, we have:

#[serde(deserialize_with = "primitive_date_time_from_str")]
pub sent: Option<PrimitiveDateTime>,

This will provide the call to our function primitive_date_time_from_str. The signature of this function is important, so you have to use it in your implementations as well. In this concrete function, we first fetch the sent field value as Option<String>. Then we define our custom format using the format_description! macro. There is also a similar function in time crate, but the benefit of using the macro is that this happens at compile time. Finally, we parse our string using the provided format and return Ok(Some(o)) in case everything is fine or just Ok(None) if there are parsing problems. You may want to return an error in your use case, but here it was enough to return None because there were no cases of errors.

Parsing JSON in Rust easily explained

Learn how to easily parse JSON data in Rust
Rust JSON

A first impression of Rust from the perspective of a Go developer

Rust is very powerful, but let's see how it compares to Go
Go Rust Kubernetes controller secret replicator