serde icon indicating copy to clipboard operation
serde copied to clipboard

Allow integer tags for internally tagged enums

Open dtolnay opened this issue 8 years ago • 16 comments

See this use case.

Would the internally tagged enum support allow me to handle schema versioning defined like this?

{
    "schema_version": 1,
    ...
}

I've never gotten a good answer about how I'd do that with Serde.

cc @ssokolow

dtolnay avatar Feb 03 '17 21:02 dtolnay

Attributes support non-string literals now right? This could be as simple as allowing:

#[derive(Serialize, Deserialize)]
#[serde(tag = "schema_version")]
enum E {
    #[serde(rename = 1)]
    V1(...),
    #[serde(rename = 2)]
    V2(...),
}

dtolnay avatar Feb 13 '17 05:02 dtolnay

Also boolean tags?

#[derive(Serialize, Deserialize)]
#[serde(tag = "error")]
enum Response {
    #[serde(rename = false)]
    Ok(QueryResult),
    #[serde(rename = true)]
    Err(QueryError),
}

dtolnay avatar Apr 15 '17 20:04 dtolnay

Any plans to progress on this feature? I could give it a try, but I would need a bit of mentoring / pointing to the right places.

Phaiax avatar Jun 30 '17 15:06 Phaiax

I have not started working on this. I would love a PR! Happy to provide guidance if you run into any trouble.

dtolnay avatar Jun 30 '17 23:06 dtolnay

Any updates on this?

Noxime avatar May 08 '18 15:05 Noxime

Hi :smile: We need this feature for sozu #240 to handle configuration versioning.

I'd like to implement it. I saw that someone (#973) started working on it but abandoned it. Can I use it as a starting point or do you recommend another approach ?

NotBad4U avatar Sep 14 '18 13:09 NotBad4U

@NotBad4U You can use a string tag (the version enum's variant name) for the configuration version.

Arnavion avatar Sep 14 '18 15:09 Arnavion

@NotBad4U I think #973 is the right approach. Literals in attributes will be stable in rust 1.30 so we can support #[serde(rename = 0)].

dtolnay avatar Sep 14 '18 15:09 dtolnay

I guess this is blocked on https://github.com/serde-rs/serde/pull/1392?

WiSaGaN avatar Dec 12 '19 10:12 WiSaGaN

For what it's worth, I think there's an additional use case for this (though it's technically not for internally tagged enums, it'd hopefully be fixed the same way): #[repr(i32)] enums and the like.

Right now my solution is https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=9154cf599592144c4473903b57d91abe ; but that's an awful lot of boilerplate for this simple use case :)

Ekleog avatar Apr 10 '20 14:04 Ekleog

@Ekleog: I was able to use the serde_repr crate suggested by official docs to shorten your playground to this:

//! ```cargo
//! [dependencies]
//! serde = "1"
//! serde_repr = "0.1"
//! serde_json = "1"
//! ```

use std::fmt;

#[derive(Copy, Clone, Debug, serde_repr::Serialize_repr, serde_repr::Deserialize_repr)]
#[repr(i32)]
pub enum Test {
    Foo = 0,
    Bar = 2,
}

fn main() {
    println!("{}", serde_json::to_string(&Test::Foo).unwrap());
    println!("{:?}", serde_json::from_str::<Test>("0").unwrap());
}

ErichDonGubler avatar Jul 13 '20 16:07 ErichDonGubler

This looks cool! I hadn't seen that in the docs when writing that message. Thank you!

Ekleog avatar Jul 13 '20 21:07 Ekleog

This issue came up in a question on Stack Overflow.

For anybody in need of a workaround for integer tags, then I answered with a workaround on Stack Overflow, using a custom serializer and deserializer, by deserializing into a serde_json::Value.

vallentin avatar Jan 05 '21 11:01 vallentin

I'm still pretty much in need of this...

for now I came up with this approach using const generics. Putting this here with the hope this might be helpful to others, or someone telling me what is wrong about it:

#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub enum Bla {
    V1 {
        hello: String,
        version: Option<Version<1>>,
    },
    V2 {
        foo: String,
        version: Version<2>,
    },
}

#[derive(Debug)]
pub struct Version<const V: u8>;

#[derive(Debug, Error)]
#[error("Invalid version")]
struct VersionError;

impl<const V: u8> Serialize for Version<V> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_u8(V)
    }
}

impl<'de, const V: u8> Deserialize<'de> for Version<V> {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let value = u8::deserialize(deserializer)?;
        if value == V {
            Ok(Version::<V>)
        } else {
            Err(serde::de::Error::custom(VersionError))
        }
    }
}

ysndr avatar Mar 01 '23 12:03 ysndr

It will be cool to have a way to be able to specify a custom deserializer for the key. In my case I have arrays containing a single string in the tag (it's weird, I know ) and I will love to be able to use it to parse my enum directly, without having to use several steps

danielo515 avatar Jun 17 '23 19:06 danielo515

@ysndr The only downside is that it doesn't fail with a nice error messages since the untagged enum will try other versions if the JSON is invalid but the version is correct. Since we don't have ContentRefDeserializer in the public API it makes it a bit hard to create a custom deserializer for the Bla enum. I guess I am waiting on https://github.com/serde-rs/serde/pull/2525 to be merged :)

In the meantime here is my solution (with schemars support):

#[derive(Clone, Debug)]
pub struct Edition<const V: u8>;

impl<const V: u8> Edition<V> {
    pub const ERROR: &'static str = "Invalid edition";
}

impl<const V: u8> PartialEq<Edition<V>> for u8 {
    fn eq(&self, _: &Edition<V>) -> bool {
        V == *self
    }
}

impl<const V: u8> Serialize for Edition<V> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_u8(V)
    }
}

impl<'de, const V: u8> Deserialize<'de> for Edition<V> {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let value = u8::deserialize(deserializer)?;
        if value == V {
            Ok(Edition::<V>)
        } else {
            Err(serde::de::Error::custom(Self::ERROR))
        }
    }
}

impl<const V: u8> JsonSchema for Edition<V> {
    fn schema_name() -> String {
        "Edition".to_owned()
    }

    fn schema_id() -> Cow<'static, str> {
        Cow::Owned(format!("Edition_{}", V))
    }

    fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> Schema {
        use schemars::schema::*;

        let mut schema = gen.subschema_for::<u8>();
        if let Schema::Object(schema_object) = &mut schema {
            if schema_object.has_type(InstanceType::Integer)
                || schema_object.has_type(InstanceType::Number)
            {
                let validation = schema_object.number();
                validation.minimum = Some(V as f64);
                validation.maximum = Some(V as f64);
            }
        }
        schema
    }
}

Then you implement a custom deserializer for your untagged enum

#[derive(Serialize)]
#[serde(untagged)]
pub enum MyObject {
    V2(v2::MyObject),
    V1(v1::MyObject),
}

impl<'de> Deserialize<'de> for MyObject {
    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        use serde::__private::de::{Content, ContentRefDeserializer};

        let content = Content::deserialize(deserializer)?;

        match v2::MyObject::deserialize(ContentRefDeserializer::<D::Error>::new(&content))
        {
            Ok(v) => return Ok(ParsableWorkflow::V2(v)),
            Err(e) if e.to_string() != Edition::<2>::ERROR => return Err(e),
            Err(_) => {}
        }

        match v1::MyObject::deserialize(ContentRefDeserializer::<D::Error>::new(&content))
        {
            Ok(v) => return Ok(ParsableWorkflow::V1(v)),
            Err(e) if e.to_string() != Edition::<1>::ERROR => return Err(e),
            Err(_) => {}
        }

        Err(serde::de::Error::custom(
            "data did not match any variant of untagged enum MyObject",
        ))
    }
}

Sytten avatar Feb 02 '24 20:02 Sytten