-
-
Notifications
You must be signed in to change notification settings - Fork 786
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
Allow integer tags for internally tagged enums #745
Comments
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(...),
} |
Also boolean tags? #[derive(Serialize, Deserialize)]
#[serde(tag = "error")]
enum Response {
#[serde(rename = false)]
Ok(QueryResult),
#[serde(rename = true)]
Err(QueryError),
} |
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. |
I have not started working on this. I would love a PR! Happy to provide guidance if you run into any trouble. |
Any updates on this? |
@NotBad4U You can use a string tag (the version enum's variant name) for the configuration version. |
I guess this is blocked on #1392? |
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): 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: I was able to use the //! ```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());
} |
This looks cool! I hadn't seen that in the docs when writing that message. Thank you! |
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 |
After deploying #1678, we saw a rise in memory consumption. We narrowed down the reason to deserialization of replay recordings, so this PR attempts to replace those deserializers with more efficient versions that do not parse an entire `serde_json::Value` to get the tag (`type`, `source`) of the enum. A custom deserializer is necessary because serde does not support [integer tags for internally tagged enums](serde-rs/serde#745). - [x] Custom deserializer for `NodeVariant`, based on serde's own `derive(Deserialize)` of internally tagged enums. - [x] Custom deserializer for `recording::Event`, based on serde's own `derive(Deserialize)` of internally tagged enums. - [x] Custom deserializer for `IncrementalSourceDataVariant`, based on serde's own `derive(Deserialize)` of internally tagged enums. - [x] Box all enum variants. ### Benchmark comparison Ran a criterion benchmark on `rrweb.json`. It does not tell us anything about memory consumption, but the reduced cpu usage points to simpler deserialization: #### Before ``` rrweb/1 time: [142.37 ms 148.17 ms 155.61 ms] ``` #### After ``` rrweb/1 time: [31.474 ms 31.801 ms 32.137 ms] ``` #skip-changelog --------- Co-authored-by: Colton Allen <[email protected]> Co-authored-by: Oleksandr <[email protected]>
I'm still pretty much in need of this... for now I came up with this approach using #[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))
}
}
} |
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 |
@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 In the meantime here is my solution (with #[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",
))
}
} |
See this use case.
cc @ssokolow
The text was updated successfully, but these errors were encountered: