json icon indicating copy to clipboard operation
json copied to clipboard

Support (Feature Request?): Custom Serializer for Option<T>::None which results in `{}` instead of `null`

Open thehappycheese opened this issue 1 year ago • 0 comments

I tried really hard to create a generic custom serializer for structs T that are serialized as JSON Objects which encode/decode Option<T>::None as {} instead of null (because this behavior is required in the jupyter messaging protocol)

Work-around

I found a non-idiomatic work-around, but it basically requires that I re-implement a custom Option type like this

#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(deny_unknown_fields)]
#[serde(untagged)]
pub enum EmptyObjectOr<T> {
    EmptyObject {},
    Object(T),
}

Where

  • EmptyObjectOr::EmptyObject is like Option::None
    • but has the desired property of being serialized to/from the JSON empty object {} instead of null
  • EmptyObjectOr::Object(T) ~~ Option::Some(T)
    • is serialized the same as serde_json::to_string(T), thanks to the flattening behaviour of serde(untagged)

Now I can define other structs like

#[derive(Debug, Deserialize, Serialize)]
pub struct MessageParsed {
    /// parent_header is sometimes a json object (de-serialized by the struct Header),
    /// sometimes its an empty json object, de-serialized by EmptyObjectOr
    pub parent_header: EmptyObjectOr<Header>,
    // ...
}

But the question is

I still really want to use the more idiomatic Option type like this:

#[derive(Debug, Deserialize, Serialize)]
pub struct MessageParsed {
    pub parent_header: Option<Header>,
    // ...
}

I tried writing a custom serializer/de-serializer using #[serde(deserialize_with="de_option_or", serialize_with="ser_option_or")] But the trouble is that the visitor cannot tell if an object is empty without consuming it, then there is no way to push it down to a lower level deserialiser in a generic way.

Click to see the closest I got

This was the closest I got, but it does not compile because map.size_hint() appears to not be able to detect there are zero keys in the object being deserialized (and probably other reasons, i forget what all the compilation errors were).

pub fn deserialize_empty_object_as_none<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
where
    D: Deserializer<'de>,
    T: Deserialize<'de>,
{
    struct EmptyObjectVisitor<T>(PhantomData<T>);

    impl<'de, T> Visitor<'de> for EmptyObjectVisitor<T>
    where
        T: Deserialize<'de>,
    {
        type Value = Option<T>;

        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
            formatter.write_str("an object or an empty object")
        }

        fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
        where
            A: MapAccess<'de>,
        {
            let is_empty = map.size_hint().map_or(true, |(size, _)| size == 0);
            if is_empty {
                Ok(None)
            } else {
                T::deserialize(serde::de::value::MapAccessDeserializer::new(map)).map(Some)
            }
        }
    }

    deserializer.deserialize_map(EmptyObjectVisitor(PhantomData))
}
  • Is my work around the best possible compromise?
  • is there a way to make the deserialize_with/serialize_with method work?
  • Is there scope for some kind of future feature like
    #[serde(option_none_as_empty_object)]

Many thanks for your time :) I decided to post here since I figured any discussion might be easier to find if others run into the same problem than if it were on discord.

thehappycheese avatar Jan 21 '24 06:01 thehappycheese