Skip to content

Enum internal tag representation seems inconsistent #1274

@f-squirrel

Description

@f-squirrel

I am not sure whether this is a bug or a feature, but I did not manage to find any explanation behind it.

If an enum is tagged internally and it is flattened in the outer struct, then the tag can be either a string or a number (position in the enum) while deserialization. The serializer always produces a string though.

If the enum is not flattened, then the tag can be only a string.

Please consider the code below:

use serde_derive::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash, Clone, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]
struct A {
    name: String,
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash, Clone, PartialOrd, Ord)]
struct B {
    num: i64,
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash, Clone, PartialOrd, Ord)]
#[serde(tag = "tx_type"/* , content = "value" */)]
#[non_exhaustive]
enum Types {
    A(A),
    B(B),
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash, Clone, PartialOrd, Ord)]
struct OuterFlattened {
    #[serde(flatten)]
    types: Types,
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash, Clone, PartialOrd, Ord)]
struct OuterUnFlattened {
    types: Types,
}

pub fn main() {
    let type_a_num = r#"{"tx_type" :0, "name":"Alice"}"#;
    let type_b_num = r#"{"tx_type" :1, "num":42}"#;

    let type_a_num: OuterFlattened = serde_json::from_str(&type_a_num).unwrap();
    let type_b_num: OuterFlattened = serde_json::from_str(&type_b_num).unwrap();

    let type_a_str = r#"{"tx_type": "A", "name":"Alice"}"#;
    let type_b_str = r#"{"tx_type": "B", "num":42}"#;

    let type_a_str: OuterFlattened = serde_json::from_str(&type_a_str).unwrap();
    let type_b_str: OuterFlattened = serde_json::from_str(&type_b_str).unwrap();

    assert_eq!(type_a_num, type_a_str);
    assert_eq!(type_b_num, type_b_str);

    println!("EQUAL!");

    let type_a_num_ = r#"{"types":{"tx_type": 0,"name":"Alice"}}"#;
    let type_a_str_ = r#"{"types":{"tx_type": "A","name":"Alice"}}"#;

    let a_num: OuterUnFlattened = serde_json::from_str(&type_a_num_).unwrap();
    let a_str: OuterUnFlattened = serde_json::from_str(&type_a_str_).unwrap();

    assert_eq!(a_num, a_str);
}

Output:

cargo r
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/serde_enum_tag_num`
EQUAL!

thread 'main' panicked at src/main.rs:54:70:
called `Result::unwrap()` on an `Err` value: Error("invalid type: integer `0`, expected variant identifier", line: 1, column: 22)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Version:

rustc --version
rustc 1.86.0 (05f9846f8 2025-03-31)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions