How to use serde_json with a union type like enum?

Viewed 2088

I have two structs that I want to serialize/deserialize with the tag as a "type" field in JSON, like so.

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
struct ThingA {
    value: usize,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
struct ThingB {
    value: usize,
}

These serialize as expected. For example,

let a = ThingA { value: 0 };
println!("{}", serde_json::to_string(&a)?);
// This yields the expected result:
// {"type":"ThingA","value":0}

However, I'm running into trouble when I try to add an enum to stand in as a union type for the structs.

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
enum Thing {
    ThingA(ThingA),
    ThingB(ThingB),
}

The definition above works fine for deserializing JSON, but adds an extra field during serialization.

let json = r#"{"type": "ThingB", "value": 0}"#;
let thing: Thing = serde_json::from_str(json)?;
// Correctly parses to:
// ThingB(ThingB { value: 0 })

println!("{}", serde_json::to_string(&thing)?);
// Incorrectly serializes with an extra "type" field:
// {"type":"ThingB","type":"ThingB","value":0}

Changing #[serde(tag = "type")] to #[serde(untagged)] on the Thing enum causes the opposite problem: Thing instances serialize properly, but don't get parsed correctly anymore.

My goal is to get JSON {"type": "ThingB", value: 0} to evaluate to Thing::ThingB(ThingB {value: 0}) during deserialization, and vice versa, but only if I'm deserializing to a Thing. If I have an unwrapped ThingB, like ThingB {value: 0}, I want it to serialize to {"type": "ThingB", value: 0} as well.

So my questions are: Is there any way to assign the serde tag or untagged attributes such that they only apply during serialization/deserialization (similar to serde's rename)? If not, any advice on how to implement Serialize and/or Deserialize to achieve my goal?

1 Answers

You can just use tag in your Thing enum, leaving the others clean:

use serde::{Serialize, Deserialize}; // 1.0.124
use serde_json; // 1.0.64

#[derive(Debug, Clone, Serialize, Deserialize)]
struct ThingA {
    value: usize,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct ThingB {
    value: usize,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
enum Thing {
    ThingA(ThingA),
    ThingB(ThingB),
}

fn main() {
    let json = r#"{"type": "ThingB", "value": 0}"#;
    let thing: Thing = serde_json::from_str(json).unwrap();
    println!("{}", serde_json::to_string(&thing).unwrap());

}

Playground

As requested in the comments. In case we would want to have both tagged (enum and structs) we would need to make some serde abracadabra playing with wrappers and with the with. More info can be found here

Related