Serde_json: Flatten nested internally-tagged enums?


#1

I’m trying to deserialize JSON into a somewhat complicated structure. The JSON itself is simple - it’s a single, flat object (all field values are either numbers, strings, or booleans). However, it can have different structures depending on what it represents.

If there were only one dimension along which this structure could exist, it’d be easy. For example, with objects like:

{"type": "foo", myInt: 3 }
{"type": "bar", myBool: false }

we could do the following:

#[derive(Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum Message {
    Foo { myInt: isize },
    Bar { myBool: bool },
}

Unfortunately, it’s more complicated than that. There are actually multiple dimensions along which the object can differ. Thus, we have something like:

{"type": "emotion_level", "message_id": 111, "emotion": "happy", "happiness": 3}
{"type": "emotion_level", "message_id": 222, "emotion": "sad", "sadness": 2}
{"type": "car", "message_id": 333, "part": "wheel", "wheels": 4}
{"type": "car", "message_id": 444, "part": "door", "doors": 2}

What I’ve tried for this is as follows, but it doesn’t work unfortunately:

#[derive(Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum Message {
    EmotionLevel { message_id: usize, emotion: Emotion },
    Car { message_id: usize, part: CarPart },
}

#[derive(Serialize, Deserialize)]
#[serde(tag = "emotion", rename_all = "snake_case")]
enum Emotion {
    Happy { happiness: isize },
    Sad { sadness: isize },
}

#[derive(Serialize, Deserialize)]
#[serde(tag = "part", rename_all = "snake_case")]
enum CarPart {
    Wheel { wheels: usize },
    Door { doors: usize },
}

Unfortunately, serde interprets this as the nested structure you’d expect. When I serialize it, I get something like:

{"type": "emotion_level", "message_id": 111, "emotion": {"emotion": "happy", "happiness": 3}}

And when I try to deserialize the first JSON message I described above, I get an error.

Is there any way to instruct serde to flatten these structures into one, or will I just have to implement custom serialization/deserialization?


#2

There is no built-in way until we implement a flatten attribute – I see you commented on serde-rs/serde#119.

For now you need to deserialize these yourself. Using your data structures from above, something like:

use serde::de::{self, Deserialize, Deserializer};
use serde_json::Value;

impl<'de> Deserialize<'de> for Message {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
        where D: Deserializer<'de>
    {
        #[derive(Deserialize)]
        #[serde(tag = "type", rename_all = "snake_case")]
        enum MessageHelper {
            EmotionLevel { message_id: usize },
            Car { message_id: usize },
        }

        let v = Value::deserialize(deserializer)?;
        let m = MessageHelper::deserialize(&v).map_err(de::Error::custom)?;
        match m {
            MessageHelper::EmotionLevel { message_id } => {
                Ok(Message::EmotionLevel {
                    message_id,
                    emotion: Emotion::deserialize(&v).map_err(de::Error::custom)?,
                })
            }
            MessageHelper::Car { message_id } => {
                Ok(Message::Car {
                    message_id,
                    part: CarPart::deserialize(&v).map_err(de::Error::custom)?,
                })
            }
        }
    }
}

#3

Awesome, that works. Thanks! Sorry for the late reply - I had put this project on the back burner and finally got around to trying out your suggestion :slight_smile: