Serde serialization with Option of external struct

I have the following struct I'd like to (de)serialize

pub struct Stopwatch {
    start_time: Option<Instant>,
}

I have a module that has a (de)serializer that can (de)serialize Instant.

How would I got about applying this to an Optional<Instant>?
Do I have to build a new (de)serializer for Option<Instant> or is there a shortcut?
Am I overthinking this or is there a simpler way to (de)serialize this struct?


My current solution, which seems very convoluted for what I'm trying to do:
Playground

I have also looked at serde_with, would that help me in this case?

You have the correct idea here, you need a separate function which takes/produces an Option<Instant>. The with attribute always applies to the whole type, be it Instant, Option<Instant> or Vec<Instant>. This means it doesn't really compose well if you want to use another collection type.
The approach of separate functions for option is also what popular crates like chrono do.

I think this is not correct and needs to be even more convoluted. During serialization you are not serializing the Some, but directly the inner value:

Some(i) => super::serialize(i, serializer),

This works ok for some formats, like JSON, but if the format is explicit about Option, like Ron, then you have a mismatch there.
During deserialization you seem to ignore the Option completely:

Ok(Some(super::deserialize(deserializer)?))

If you remove the skip_serializing_if = "Option::is_none" then you will notice that deserializing a None will fail with invalid type: null, expected struct SystemTime.

Yes, one of the goals of serde_with is to "solve" the composability issue of the with attribute. The relevant part is this secion of the user guide: serde_with::guide::serde_as - Rust
You can reuse your mod instant_serializer but will have to add a bit of boilerplate, which is all behind the link. After that, you can write:

#[serde_with::serde_as]
pub struct Stopwatch {
    #[serde(default)]
    #[serde_as(as = "Option<MyInstant>")]
    start_time: Option<Instant>,
}
1 Like

A considerably simpler and correct solution is to write a newtype wrapper.

@jonasbb

wow thank you for your answer! For my serialization and deserialization, how would I go about implementing correctly? As I understand it, I should be using serializer.serialize_some()? Though I'm not sure how to combine it with my non option serializer.

As for the deserialization, would an implementation like this be more correct?

let option: Option<String> = Option::deserialize(deserializer)?;
if let Some(i) = option {
    return Ok(Some(super::deserialize(i.into_deserializer())?));
}
Ok(None)

@H2CO3

I like this approach because it eliminates the boilerplate for the extra Option<Instant> (de)serializer; though now in your struct you have to work with your wrapper and convert it first into the non wrapped type, which seems a little strange, because I feel like the struct shouldn't have to "know" that it is being serialized externally.

Is there a reason you added the implementations for the From trait but didn't use it? Would it be more rustacean to convert from and to the wrapper using those traits or directly (un)wrapping it? (The code for using the traits would be slightly more code, adding to that necessary conversation in the struct's methods I mentioned)


I think serde_with is a nice middle ground between these two approaches, really the only boilerplate I needed for that is this (provided I implemented it correctly)

struct MyInstant;

impl SerializeAs<Instant> for MyInstant {
    fn serialize_as<S>(source: &Instant, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        instant_serializer::serialize(source, serializer)
    }
}

impl<'de> DeserializeAs<'de, Instant> for MyInstant {
    fn deserialize_as<D>(deserializer: D) -> Result<Instant, D::Error>
    where
        D: Deserializer<'de>,
    {
        instant_serializer::deserialize(deserializer)
    }
}

Playground
Note: The code will not run online, as serde_with is not included in Rust Playground.

Yes you need to call serialize_some. There is no direct way for you to call the normal serializer module. You can use a newtype wrapper to connect these two, roughly like this:

pub fn serialize<S>(instant: &Option<Instant>, serializer: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    #[derive(serde::Serialize)]
    #[serde(transparent)]
    struct W<'a>(#[serde(with = "super")] &'a Instant)

    match instant {
        Some(i) => serializer.serialize_some(&W(i))
        None => serializer.serialize_none(),
    }
}

No, this deserializes an Option<String> and not an Option<SystemTime>. The SystemTime turns multiple nested objects when serialized into JSON, e.g., {"start_time":{"secs_since_epoch":1637711529,"nanos_since_epoch":603522028}}, which is not the same as a String, which is a native type in JSON. It will not deserialize like this.

You can see how much code it is to implement Deserialize fully in this chrono code. You need to write at least one Visitor or use the serde derives. You can use the same temporary wrapper type here too:

pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Instant>, D::Error>
where
    D: Deserializer<'de>,
{
    #[derive(serde::Deserialize)]
    #[serde(transparent)]
    struct W(#[serde(with = "super")] Instant)

    Ok(Option::deserialize(deserializer)?.map(|W(inner)| inner))
}

Notice how the definition of W is different in both cases, since for serialization you only ever have a reference, but for deserialization you want a value.

If you already have a module which works for serde with this is all the boilerplate needed. It can also easily be encapsulated in a macro_rules macro to avoid repeating the boilerplate. If you start out directly with serde_with there is not any additional boilerplate. You trade a module with two functions with a type and two single function trait implementations.

I'm not sure what is strange about it. The Instant type doesn't know how to serialize itself. Since it's from a downstream crate, as well as Serialize, you can't implement Serialize on it directly. If you want to serialize it through Serde traits anyway, you pretty much have to create a newtype.

I added them so that downstream code can use it. It's not that it's better to write InstantWrapper::from(instant) in literal code – it's primarily helpful in a generic context.

1 Like