obake icon indicating copy to clipboard operation
obake copied to clipboard

Deserialzing `untagged` versions should default to latest version (not earliest)

Open joaoantoniocardoso opened this issue 1 year ago • 1 comments

Hi! This library is so useful, but I'm failing to use it with serde_json. I've tried the following way:


use serde_json::json;
use obake;

#[obake::versioned]
#[obake(version("0.1.0"))]
#[obake(version("0.2.0"))]
#[obake(version("0.3.0"))]
#[obake(derive(Debug, PartialEq, serde::Serialize, serde::Deserialize))]
#[obake(serde(untagged))]
#[derive(Debug, Default, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
struct Character {
    name: String,
    age: u32,
    #[obake(cfg(">=0.2.0"))]
    height: f32,
    #[obake(cfg(">=0.3.0"))]
    weight: f32,
}

impl From<Character!["0.1.0"]> for Character!["0.2.0"] {
    fn from(old: Character!["0.1.0"]) -> Self {
        Self {
            name: old.name,
            age: old.age,
            ..Default::default()
        }
    }
}

impl From<Character!["0.2.0"]> for Character!["0.3.0"] {
    fn from(old: Character!["0.2.0"]) -> Self {
        Self {
            name: old.name,
            age: old.age,
            height: old.height,
            ..Default::default()
        }
    }
}

fn main() {}

#[cfg(test)]
mod tests {
    use serde_json::json;
    use crate::*;

    #[test]
    fn from_2_to_3() {
        let freeza_ser = serde_json::to_string_pretty(&json!({
            "name": "Freeza",
            "age": 32,
            "height": 1.53,
        })).unwrap();
        dbg!(&freeza_ser);

        let freeza: VersionedCharacter = serde_json::from_str(&freeza_ser).unwrap();
        dbg!(&freeza);
        let freeza: Character = freeza.into();
        dbg!(&freeza);

        let freeza_expected = Character {
            name: "Freeza".into(),
            age: 32,
            height: 1.53,
            weight: 0.,
        };
        assert_eq!(&freeza_expected, &freeza);
    }
}

The output is:

failures:

---- tests::from_2_to_3 stdout ----
[src/main.rs:56] &freeza_ser = "{\n  \"age\": 32,\n  \"height\": 1.53,\n  \"name\": \"Freeza\"\n}"
[src/main.rs:59] &freeza = Character_v0_1_0(
    Character_v0_1_0 {
        name: "Freeza",
        age: 32,
    },
)
[src/main.rs:61] &freeza = Character_v0_3_0 {
    name: "Freeza",
    age: 32,
    height: 0.0,
    weight: 0.0,
}
thread 'tests::from_2_to_3' panicked at 'assertion failed: `(left == right)`
  left: `Character_v0_3_0 { name: "Freeza", age: 32, height: 1.53, weight: 0.0 }`,
 right: `Character_v0_3_0 { name: "Freeza", age: 32, height: 0.0, weight: 0.0 }`', src/main.rs:69:9

So, the current behavior is that serde_json::from_str::<VersionedCharacter>(&freeza_ser) is deserializing into the first version, and I want it to deserialize like the following:

fn character_parser(serialized_data: &String) -> Character {
    if let Ok(data) = serde_json::from_str::<Character!["0.3.0"]>(&serialized_data) {
        return data;
    }

    if let Ok(data) = serde_json::from_str::<Character!["0.2.0"]>(&serialized_data) {
        let data: Character!["0.3.0"] = data.into();
        return data;
    }

    if let Ok(data) = serde_json::from_str::<Character!["0.1.0"]>(&serialized_data) {
        let data: Character!["0.2.0"] = data.into();
        let data: Character!["0.3.0"] = data.into();
        return data;
    }

    return Character::default();
}

Am I missing something crucial here? Is there any way for this lib to implement that parsing automatically?

Thank you!

joaoantoniocardoso avatar Jul 22 '22 14:07 joaoantoniocardoso

This is very interesting thank you! I believe the issue has to do with the ambiguity caused by untagged. The behaviour you're observing is that the deserializer is trying earlier versions first, when in actual fact we'd like it to try later versions first. I'll have a look at this as soon as possible!

doctorn avatar Aug 25 '22 14:08 doctorn