Deserializing optional date times with Serde
April 30th, 2019
As part of the CLI app I mentioned previously I used Serde to parse a JSON response from a HTTP API. The response has an optional date time value with a custom format. It took a little while for me to figure out how to handle it so I thought I’d write it up here.
The problem
The response has a last_updated
field that is either not present or a UTC date time in a custom format. The chrono crate handles date times with the DateTime<Utc>
type. As the date time value might not be present in the response the type in the struct is Option<DateTime<Utc>>
.
The struct looks like:
#[derive(Deserialize, Debug)]
struct DataResponse {
: Option<String>,
error_message#[serde(default)]
#[serde(deserialize_with = "deserializer::deserialize_optional_datetime")]
: Option<DateTime<Utc>>,
last_updated: Vec<DataPoint>,
rows: DataResponseStatus,
status}
The dates look like 07-Mar-2019 21:00:00
. There is another field in the response that is always present with the same date time format.
The solution
Using deserialize_with
tells Serde to use custom deserialization code for the last_updated
field. The deserialize_optional_datetime
code is:
pub fn deserialize_optional_datetime<'de, D>(d: D) -> Result<Option<DateTime<Utc>>, D::Error>
where
: de::Deserializer<'de>,
D{
.deserialize_option(OptionalDateTimeFromCustomFormatVisitor)
d}
The types should be familiar if you’ve used Serde before. If you’re not used to Rust then the function signature will likely look a bit strange. What it’s saying is that d
will be something that implements Serde’s Deserializer
trait, and that any references to memory will live for the 'de
lifetime.
You don’t need to understand all this fully before you can use Serde usefully though it will help when you want to do more advanced serialization and deserialization.
The code calls d.deserialize_option
passing in a struct. The passed in struct implements Serde’s Visitor
trait and looks like:
struct OptionalDateTimeFromCustomFormatVisitor;
impl<'de> de::Visitor<'de> for OptionalDateTimeFromCustomFormatVisitor {
type Value = Option<DateTime<Utc>>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "null or a datetime string")
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
: de::Error,
E{
Ok(None)
}
fn visit_some<D>(self, d: D) -> Result<Option<DateTime<Utc>>, D::Error>
where
: de::Deserializer<'de>,
D{
Ok(Some(d.deserialize_str(DateTimeFromCustomFormatVisitor)?))
}
}
The struct doesn’t have any fields, it just a type we can use to implement the custom visitor. Serde’s Visitor
trait has default implementations for all it’s visit_*
functions so only visit_none
and visit_some
need to be implemented.
visit_none
returns Ok(None)
so the last_updated
value in the struct will be None
.
visit_some
delegates the parsing to another custom deserializer via the deserialize_str
call. It looks like:
struct DateTimeFromCustomFormatVisitor;
pub fn deserialize<'de, D>(d: D) -> Result<DateTime<Utc>, D::Error>
where
: de::Deserializer<'de>,
D{
.deserialize_str(DateTimeFromCustomFormatVisitor)
d}
impl<'de> de::Visitor<'de> for DateTimeFromCustomFormatVisitor {
type Value = DateTime<Utc>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "a datetime string")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
: de::Error,
E{
match NaiveDateTime::parse_from_str(value, "%d-%b-%Y %H:%M:%S") {
Ok(ndt) => Ok(DateTime::from_utc(ndt, Utc)),
Err(e) => Err(E::custom(format!("Parse error {} for {}", e, value))),
}
}
}
The main part is in visit_str
which parses the value using NaiveDateTime::parse_from_str
from chrono
. If it parses successfully it’s converted into a DateTime<Utc>
or the Err
will get propagated back up to Serde and reported.
Summary
Serde has worked well for me for what I’ve used it for so far. This is the first time I’ve needed some customisation and it took me a little bit of time to understand what I needed to do.
The visitor pattern is a well established way of handling code like this and makes a lot of sense here. For a relatively new Rust programmer there are a lot of types here but once you get your head round them it’s possible to see exactly what they’re specifying and how they work.