jsonrustserde

Eliding boilerplate parent JSON key with serde


I'm really impressed with how elegantly and easily serde / serde_json can parse messages:

{"msg_type": "asauce", "aaa": 3, "bbb": 14}
{"msg_type": "csyrup", "ccc": 10, "ddd": 20}
// this works!
#[derive(serde::Deserialize, PartialEq, Debug)]
struct AppleSauce {
    aaa: u8,
    bbb: u8,
}

#[derive(serde::Deserialize, PartialEq, Debug)]
struct ChocolateSyrup {
    ccc: u8,
    ddd: u8,
}

#[derive(serde::Deserialize, PartialEq, Debug)]
#[serde(tag = "msg_type")]
enum Sauce {
    #[serde(rename = "asauce")]
    AppleSauce(AppleSauce),
    #[serde(rename = "csyrup")]
    ChocolateSyrup(ChocolateSyrup),
}

#[test]
fn deserialise_inner_applesauce() {
    let json = r#"{"msg_type": "asauce", "aaa": 3, "bbb": 14}"#;
    let expected = AppleSauce { aaa: 3, bbb: 14 };
    let sauce: Sauce = serde_json::from_str(json).unwrap();
    assert_eq!(sauce, Sauce::AppleSauce(expected));
}

However my messages have a parent key, which I'd like to be rid of:

{"boilerplate": {"msg_type": "asauce", "aaa": 3, "bbb": 14}}
{"boilerplate": {"msg_type": "csyrup", "ccc": 10, "ddd": 20}}

For the moment I've implemented a from_str method:

impl Sauce {
    pub fn from_str(msg: &str) -> Option<Sauce> {
        let outer: serde_json::Value = serde_json::from_str(msg).ok()?;
        let inner = &outer["boilerplate"];
        serde_json::from_value(inner.clone()).ok()
    }
}

#[test]
fn deserialise_boilerplate_applesauce_with_helper_method() {
    let json = r#"{"boilerplate": {"msg_type": "asauce", "aaa": 3, "bbb": 14}}"#;
    let expected = AppleSauce { aaa: 3, bbb: 14 };
    let sauce = Sauce::from_str(json).unwrap(); // not serde_json::from_str :(
    assert_eq!(sauce, Sauce::AppleSauce(expected));
}

Is there a way to elide the parent key with a serde attribute, and eliminate the custom from_str method?

I only need to deserialise these messages (but from what I see of serde, it would probably serialise too for free).


Solution

  • You can make structs (SauceSerde and SauceWrapper) that reflect the JSON, and then impl From<SauceWrapper> for Sauce to handle the conversion. Then, you can add #[serde(from = "SauceWrapper")] so that deserializing Sauce uses SauceWrapper's deserialization logic.

    #[derive(serde::Deserialize, PartialEq, Debug)]
    pub struct AppleSauce {
        aaa: u8,
        bbb: u8,
    }
    
    #[derive(serde::Deserialize, PartialEq, Debug)]
    pub struct ChocolateSyrup {
        ccc: u8,
        ddd: u8,
    }
    
    #[derive(serde::Deserialize, PartialEq, Debug)]
    #[serde(tag = "msg_type")]
    // Rename this and keep it private
    enum SauceSerde {
        #[serde(rename = "asauce")]
        AppleSauce(AppleSauce),
        #[serde(rename = "csyrup")]
        ChocolateSyrup(ChocolateSyrup),
    }
    
    // Make a copy and use `from = "SauceWrapper"`
    // Also put `into = "SauceWrapper"` if serializing is needed
    #[derive(serde::Deserialize, PartialEq, Debug)]
    #[serde(from = "SauceWrapper")]
    pub enum Sauce {
        AppleSauce(AppleSauce),
        ChocolateSyrup(ChocolateSyrup),
    }
    
    // Make a private wrapper to match the JSON
    #[derive(serde::Deserialize, PartialEq, Debug)]
    struct SauceWrapper {
        boilerplate: SauceSerde,
    }
    
    impl From<SauceWrapper> for Sauce {
        fn from(value: SauceWrapper) -> Self {
            match value.boilerplate {
                SauceSerde::AppleSauce(apple_sauce) => Sauce::AppleSauce(apple_sauce),
                SauceSerde::ChocolateSyrup(chocolate_syrup) => Sauce::ChocolateSyrup(chocolate_syrup),
            }
        }
    }
    
    #[test]
    fn deserialise_inner_applesauce() {
        let json = r#"{"boilerplate": {"msg_type": "asauce", "aaa": 3, "bbb": 14}}"#;
        let expected = AppleSauce { aaa: 3, bbb: 14 };
        let sauce: Sauce = serde_json::from_str(json).unwrap();
        assert_eq!(sauce, Sauce::AppleSauce(expected));
    }