3

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?

danwoz
  • 75
  • 2
  • 6

1 Answers1

4

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

Netwave
  • 40,134
  • 6
  • 50
  • 93
  • Thanks for the reply. The problem is, I want a `ThingA` not wrapped in the `Thing` enum to also have a `"type": "ThingA"` when serialized to JSON. – danwoz Apr 06 '21 at 16:44
  • @danwoz then probably you would need another wrapper that uses tagging for when they are single used instead of with the enum. – Netwave Apr 06 '21 at 16:57
  • Or implement a foreign serialized version. – Netwave Apr 06 '21 at 17:03
  • Ah, the wrapper idea is interesting. What's a foreign serialized version? Would that be implementing `Serialize`/`Deserialize` manually? – danwoz Apr 06 '21 at 17:23
  • @danwoz, it would be a combination of different things. A wrapper type + serialize with to add the internal tagging. Check some of the things you would need [here](https://serde.rs/remote-derive.html) – Netwave Apr 06 '21 at 17:37
  • Thanks! I'm going to accept this answer, but could you add some of the info from your comments into the answer for future readers? – danwoz Apr 07 '21 at 05:01