Search code examples
rustserde

How can I distinguish between a deserialized field that is missing and one that is null?


I'd like to use Serde to parse some JSON as part of a HTTP PATCH request. Since PATCH requests don't pass the entire object, only the relevant data to update, I need the ability to tell between a value that was not passed, a value that was explicitly set to null, and a value that is present.

I have a value object with multiple nullable fields:

struct Resource {
    a: Option<i32>,
    b: Option<i32>,
    c: Option<i32>,
}

If the client submits JSON like this:

{"a": 42, "b": null}

I'd like to change a to Some(42), b to None, and leave c unchanged.

I tried wrapping each field in one more level of Option:

#[derive(Debug, Deserialize)]
struct ResourcePatch {
    a: Option<Option<i32>>,
    b: Option<Option<i32>>,
    c: Option<Option<i32>>,
}

playground

This does not make a distinction between b and c; both are None but I'd have wanted b to be Some(None).

I'm not tied to this representation of nested Options; any solution that can distinguish the 3 cases would be fine, such as one using a custom enum.


Solution

  • Quite likely, the only way to achieve that right now is with a custom deserialization function. Fortunately, it is not hard to implement, even to make it work for any kind of field:

    fn deserialize_optional_field<'de, T, D>(deserializer: D) -> Result<Option<Option<T>>, D::Error>
    where
        D: Deserializer<'de>,
        T: Deserialize<'de>,
    {
        Ok(Some(Option::deserialize(deserializer)?))
    }
    

    Then each field would be annotated as thus:

    #[serde(deserialize_with = "deserialize_optional_field")]
    a: Option<Option<i32>>,
    

    You also need to annotate the struct with #[serde(default)], so that empty fields are deserialized to an "unwrapped" None. The trick is to wrap present values around Some.

    Serialization relies on another trick: skipping serialization when the field is None:

    #[serde(deserialize_with = "deserialize_optional_field")]
    #[serde(skip_serializing_if = "Option::is_none")]
    a: Option<Option<i32>>,
    

    Playground with the full example. The output:

    Original JSON: {"a": 42, "b": null}
    > Resource { a: Some(Some(42)), b: Some(None), c: None }
    < {"a":42,"b":null}