[Solved] Serde `deserialize_with` for `Option`s

I'm trying to use serde to deserialize some JSON that has datetimes specified as unix timestamps (in seconds), for example:

{ "time": 1501285943 }

I'm using chrono which has opt-in support for this via a ts_seconds helper, and is intended to be used as follows:
https://github.com/chronotope/chrono/blob/d99304145b7c3d183ff5355e71a080d3a9b91712/src/datetime.rs#L723-L728

This works, but in my case, the field could have a null value, and I'm not sure how to change the example to work with Option<DateTime<Utc>> instead.

So in general, my question is, if I have a way to deserialize a field to a type T, is there a straightforward way to reuse that to deserialize a field to type Option<T>?

I won't operate on chrono directly, as I will admit it's easier for me to test ideas in playpen. That said, this should be possible to do with chrono too.

Let's say that this is the current code.

extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;

use serde::{Deserialize, Deserializer};

#[derive(Debug, Deserialize)]
struct S {
    #[serde(deserialize_with = "callback")] s: i32,
}

fn callback<'de, D>(deserializer: D) -> Result<i32, D::Error>
where
    D: Deserializer<'de>,
{
    Ok(i32::deserialize(deserializer)? * 2)
}

fn main() {
    println!("{:?}", serde_json::from_str::<S>(r#"{"s": 42}"#));
}

But, now a custom deserialization for i32 is needed to handle Option<i32>. It's possible to provide a wrapper type to deal with this issue. I will just change definition of S struct here to refer to a single field struct.

#[derive(Debug, Deserialize)]
struct S {
    s: Option<WrappedI32>,
}

#[derive(Debug, Deserialize)]
struct WrappedI32(#[serde(deserialize_with = "callback")] i32);

The access to that field now requires unpacking (Some(WrappedI32(value)) => ...,) or using .0 syntax. There are ways to avoid this, but they are uglier (involving writing a deserializer callback function for Option<_>).

2 Likes

Thanks! I was hoping there was a way to do it without needing to resort to using a newtype as the final output -- normally I'd be fine with it if the containing struct were part of some internal interface, but unfortunately I intend for it to be in the public-facing part of a library. It would be a bit cumbersome for users to have to deal with a wrapper type (when the existence of the wrapper is just an implementation detail around how the data was deserialized).

I'll continue to think about it, but thanks for the answer! I might just end up going that route if I can't find another way to do it.

Ok, figured out a way to do it, that I'm satisfied with. Building on @xfix's example:

I define another function, which uses the generated deserializer for WrappedI32 (full type annotations included for clarity):

pub fn callback_opt<'de, D>(deserializer: D) -> Result<Option<i32>, D::Error>
where
    D: Deserializer<'de>,
{
    Option::<WrappedI32>::deserialize(deserializer)
        .map(|opt_wrapped: Option<WrappedI32>| {
            opt_wrapped.map(|wrapped: WrappedI32| wrapped.0)
        })
}

and then I change the final output struct to use this function:

#[derive(Debug, Deserialize)]
struct S {
    #[serde(deserialize_with = "callback_opt")]
    s: Option<i32>,
}

So the final output struct doesn't need to contain the WrappedI32 newtype at all! :tada:

And for completeness, the playground link is here:

3 Likes

I just came across a problem of the same shape when trying to deserialize an optional date from a fixed format.

I managed to remove the need for the wrapper type.

I've updated your playground example with that simplification. Rust Playground

lemme know if I've missed something in what you were attempting.

Please don't bump old threads.

Ok, your example is the same =)
But if you remove "s2": null from &str you will get error about missing value. You must put #[serde(default)] before #[serde(deserialize_with = "callback_opt")], so no transparent solution =(

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.