'Deserialize using a function of the tag

An API with this internally tagged field structure, with "topic" being the tag:

{
    "topic": "Car"
    "name": "BMW"
    "HP": 250
}

This can be deserialized with

#[derive(Serialize, Deserialize)]
#[serde(tag = "topic")]
pub enum catalog {
    CarEntry(Car),
    ... (other types)
}

#[derive(Serialize, Deserialize)]
pub struct Car {
    pub name: String
    pub HP: i32
}

It turns out that instead of reporting the topic as just Car, the API actually sends Car.product1 or Car.product2 etc.

This breaks the deserialization, because the deserializer doesn't know what the type is based on the string. Is there a way to supply a function to chop off the type string so that the correct model is found?



Solution 1:[1]

I don't think serde provides a way to mangle the tag before using it (at least I don't see anything relevant). And the generated serializers for tagged enums are relatively complex, with internal caching if the tag isn't the first field, and whatnot, so I wouldn't want to reproduce that in a custom deserializer.

The cheapest (but not necessarily most efficient) shot at this is to deserialize to serde_json::Value first, manually process the tag, and then deserialize the serde_json::Values to whatever struct you want.

Do that in a custom deserializer, and it starts looking reasonable:

impl<'de> Deserialize<'de> for Catalog {
    fn deserialize<D>(d: D) -> Result<Self, <D as Deserializer<'de>>::Error>
    where
        D: Deserializer<'de>,
    {
        use serde_json::{Map, Value};
        #[derive(Deserialize)]
        struct Pre {
            topic: String,
            #[serde(flatten)]
            data: Map<String, Value>,
        }
        let v = Pre::deserialize(d)?;
        // Now you can mangle Pre any way you want to get your final structs.
        match v.topic.as_bytes() {
            [b'C', b'a', b'r', b'.', _rest @ ..] => Ok(Catalog::CarEntry(
                serde_json::from_value(v.data.into()).map_err(de::Error::custom)?,
            )),
            [b'B', b'a', b'r', b'.', _rest @ ..] => Ok(Catalog::BarEntry(
                serde_json::from_value(v.data.into()).map_err(de::Error::custom)?,
            )),
            _ => return Err(de::Error::unknown_variant(&v.topic, &["Car.…", "Bar.…"])),
        }
    }
}

Playground

Btw, what do you want to do with the suffix of topic? Throw it away? How do you plan on handling serialization if you do throw it away?

Solution 2:[2]

You can directly use enum instead of defining extra struct type.

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "topic")]
pub enum Catalog {
    Car { name: String, hp: i32 }
}

fn main() {
    let car = Catalog::Car { name: String::from("BMW"), hp: 2000 };

    // Convert the Car to a JSON string.
    let serialized = serde_json::to_string(&car).unwrap();

    // Prints serialized = {"topic":"Car","name":"BMW","hp":2000}
    println!("serialized = {}", serialized);

    // Convert the JSON string back to a Car.
    let deserialized: Catalog = serde_json::from_str(&serialized).unwrap();

    // Prints deserialized = Car { name: "BMW", hp: 2000 }
    println!("deserialized = {:?}", deserialized);
}

Playground


You can use #[serde(rename()] to rename type in output

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "topic")]
pub enum Catalog {
    #[serde(rename(serialize = "Car", deserialize = "CarEntry"))]
    CarEntry(Car),
}

#[derive(Serialize, Deserialize, Debug)]
pub struct Car {
    pub name: String,
    pub hp: i32
}

fn main() {
    let car = Car { name: String::from("BMW"), hp: 2000 };
    let catalog = Catalog::CarEntry(car);
    // Convert the Car to a JSON string.
    let serialized = serde_json::to_string(&catalog).unwrap();

    // Prints serialized = {"topic":"Car","name":"BMW","hp":2000}
    println!("serialized = {}", serialized);

    // Convert the JSON string back to a Car.
    let deserialized: Car = serde_json::from_str(&serialized).unwrap();

    // Prints deserialized = Car { name: "BMW", hp: 2000 }
    println!("deserialized = {:?}", deserialized);
}

Playground

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Caesar
Solution 2 Chandan