Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: force option to serialize as variants (with = option_as_enum) #365

Open
CAD97 opened this issue Sep 23, 2021 · 3 comments
Open

feat: force option to serialize as variants (with = option_as_enum) #365

CAD97 opened this issue Sep 23, 2021 · 3 comments
Labels
enhancement New feature or request

Comments

@CAD97
Copy link

CAD97 commented Sep 23, 2021

This is one way to e.g. successfully roundtrip things like Option<Option<u32>> into formats that have unwrap_or_null behavior by default (as opposed to e.g. double_option or unwrap_or_skip).

Example implementation:

use serde; // 1.0.130
use serde_json; // 1.0.67

mod option_as_enum {
    pub fn serialize<T, S>(value: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::ser::Serializer,
        T: serde::ser::Serialize,
    {
        match value {
            None => serializer.serialize_unit_variant("Option", 0, "None"),
            Some(value) => serializer.serialize_newtype_variant("Option", 1, "Some", value),
        }
    }
    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
    where
        T: serde::de::Deserialize<'de>,
        D: serde::de::Deserializer<'de>,
    {
        #[derive(serde::Deserialize)]
        #[serde(rename = "Option")]
        enum Maybe<T> {
            None,
            Some(T),
        }
        match serde::de::Deserialize::deserialize(deserializer)? {
            Maybe::None => Ok(None),
            Maybe::Some(value) => Ok(Some(value)),
        }
    }
}

#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct S {
    #[serde(with = "option_as_enum")]
    f: Option<Option<u32>>,
}

fn main() {
    let vs = [
        S { f: None },
        S { f: Some(None) },
        S { f: Some(Some(0)) },
    ];
    let serialized = serde_json::to_value(&vs).unwrap();
    let deserialized: [S; 3] = serde_json::from_value(serialized.clone()).unwrap();
    println!("starting: {:#?}\nserialized: {:#}\ndeserialized: {:#?}", vs, serialized, deserialized);
}

Output, showing behavior (manually reformatted):

starting: [
    S { f: None, },
    S { f: Some(None) },
    S { f: Some(Some(0)) },
]
serialized: [
  { "f": "None" },
  { "f": { "Some": null } },
  { "f": { "Some": 0 } }
]
deserialized: [
    S { f: None, },
    S { f: Some(None) },
    S { f: Some(Some(0)) },
]
@jonasbb
Copy link
Owner

jonasbb commented Sep 23, 2021

This seems like a useful addition. I think I would prefer this to exists as a serde_as compatible type, which allows it to be used for nested types. This could be used for Vec<Option<T>> or for Option<Option<Option<T>>>, if you want to ensure that all three layers of Options are serialized.

Would there be a benefit to also offer different enum representations, i.e., internally and adjacently tagged? Externally tagged is the more efficient one and does also work with non self-describing formats.

@jonasbb jonasbb added the enhancement New feature or request label Sep 23, 2021
@CAD97
Copy link
Author

CAD97 commented Sep 23, 2021

I don't think internal/adjacent tagging would see any use; it's not really useful to have such structure for a unit variant, imho. But maybe I'm wrong; if the implementation effort is minimal enough, it might be worth providing just for completeness. That said, I do think providing the different ways to map Option to other common enum representations in the serde data model is a useful thing to provide.

  • unwrap_or_skip
    • none => {nothing}
    • some => {inner}
  • unwrap_or_unit (untagged)
    • none => unit
    • some => {inner}
  • as_option_variant (externally tagged)
    • none => unit_variant
    • some => newtype_variant
  • unwrap_or_tuple
    • none => unit
    • none => tuple { 0: {inner} } (this coerces transparent-newtype (encouraged) formats to emit a wrapper)
  • (internal, adjacent tagging)

@jonasbb
Copy link
Owner

jonasbb commented Sep 26, 2021

I think the listed variants work ok and could be added. unwrap_or_skip and unwrap_or_unit seem very similar, though, and would only differ in the deserialization behavior. They can use the same serialization function, but in the unwrap_or_skip case it would never be called.
For deserialization, you could also use the same function, which would permit deserialize "foobar": null in the unwrap_or_skip case. This might or might not be desired.
Other than that, the difference is mainly in the serde attributes which need to be applied to a field.

/// unwrap_or_skip
#[serde(with = "unwrap_or_skip", skip_serializing_if = "Option::is_none", default)]
foobar: Option<i32>,

/// unwrap_or_unit
#[serde(with = "unwrap_or_unit")]
foobar: Option<i32>,

If there is no immediate desire to add internal and adjacent variants, I would not implement them until they are requested.

Do you want to start implementing some of these variants and send a PR? I can assist too. As mentioned, I would prefer them to be structs implementing SerializeAs/DeserializeAs traits.

jonasbb added a commit that referenced this issue Nov 5, 2022
Add new conversions UntaggedOption and ExplicitOption.
UntaggedOption serializes the enum but untagged, sidestepping the
default Option rules.
ExplicitOption serializes the Option as any other enum.

Fixes #365
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants