Skip to content

Support, or at least document the "double option" pattern. #1042

@golddranks

Description

@golddranks

When developing services that communicate with JSON or similar flexible data formats, it's a common pattern to communicate with "changesets"; or updates to state with only the changed fields present. To represent this kind of data in Rust one can have a struct with every field wrapped as Option<T>, where the None variant signifies a missing field. For one, Diesel supports this pattern when updating data in the database.

However, when the datatype itself is nullable, the meaning of a "missing field" (=don't update) and "null value" (= update by setting to null) get conflated. Fortunately, there is a pattern to support this: double option: Option<Option<T>>. Here the outer option signifies the existence of the field, and the inner option represents the value, or a null.

Serde doesn't support this pattern by default; the visitor visits only the outer Option, and sets it None in case of the default behaviour. However, I think that it would be valuable to support the double option pattern, where it tries to set the inner Option.

Below is an implementation that I'm using with #[serde(deserialize_with = "double_option")].

In case of Option<Option<T>> at the moment, the inner Option never gets set as None – it's essentially transparent, and thus serves no purpose. So I see the double option pattern as more sensible for this case.


pub fn double_option<'de, T: Deserialize<'de>, D>(de: D) -> Result<Option<Option<T>>, D::Error>
    where D: Deserializer<'de>
{
    #[derive(Debug)]
    struct DoubleOptionVisitor<T> {
        _inner: PhantomData<T>,
    };

    impl<'de, T: Deserialize<'de>> Visitor<'de> for DoubleOptionVisitor<T> {
        type Value = Option<Option<T>>;
        fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
            write!(formatter,
                   "Either a missing field, a field with null/None, or a field with value T")
        }
        fn visit_none<E>(self) -> Result<Self::Value, E>
            where E: Error
        {
            Ok(Some(None))
        }
        fn visit_some<D>(self, de: D) -> Result<Self::Value, D::Error>
            where D: Deserializer<'de>
        {
            match T::deserialize(de) {
                Ok(val) => Ok(Some(Some(val))),
                Err(e) => Err(e),
            }
        }
    }
    de.deserialize_option(DoubleOptionVisitor::<T> { _inner: PhantomData })
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions