Search code examples
rustserde

How to implement the default deserialiazation of a Rust Enum in custom deserialization


I am a new learner of Rust and currently slowly moving a Python project to Rust. To store data between two runs of the software, I serialized my data in YAML. But I'm currently stuck on the deserialization of old file produced by Python.

One of my type is defined as following in Rust

use serde::{Serialize, Deserialize};

#[derive(Serialize, Debug, Copy, Clone)]
pub enum Symbol {
    #[serde(alias = "BOW")]
    Bow,
    #[serde(alias = "SWORD")]
    Sword,
    Poison(u8),
    // Many more variant
}

So far, it's work pretty well with serializing and deserializing when using files generated by Rust.

Sadly, when I chose the serialization way in Python, I serialized the poison in the shape Poison(X), so to handle this, I need to implement a custom deserialization method. For reference, the same data in YAML serialized by Rust and by Python

# With Rust
symbols:
  - Bow
  - Poison: 10
# With Python
symbols:
  - BOW
  - POISON(10)

I want my Rust program to be able to read files output by Python as well as the ones output by Rust; people use this program and I don't want to break backward compatibility (but I don't have forward-compatibility).

So, I started to write my custom deserialization and used to OOP in Python, I would want my deserialization to inherit the default behavior. But Rust does not provide OOP, so I would have to write a code to mimic the default deserialization. And I don't want to do this part, as I am pretty sure, serde's developers made it way better than I would.

So, I'm asking you if you would happen to have an idea of how to do it without re-coding the default behavior.


Solution

  • There are multiple ways to implement this. The easiest IMHO is to deserialize the struct as normal, and fall back in case of a string.

    Like the following:

    #[derive(Serialize, Deserialize, Debug, Copy, Clone)]
    pub enum Symbol {
        #[serde(alias = "BOW")]
        Bow,
        #[serde(alias = "SWORD")]
        Sword,
        Poison(u8),
        // Many more variant
    }
    
    #[derive(Serialize, Debug, Copy, Clone)]
    #[serde(transparent)]
    pub struct SymbolWrapper(pub Symbol);
    
    impl<'de> Deserialize<'de> for SymbolWrapper {
        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
        where
            D: serde::Deserializer<'de>,
        {
            #[derive(Deserialize)]
            #[serde(untagged)]
            enum Format {
                New(Symbol),
                Old(String),
            }
    
            let data = Format::deserialize(deserializer)?;
            match data {
                Format::New(symbol) => Ok(SymbolWrapper(symbol)),
                Format::Old(description) => {
                    let Some(description) = description.strip_suffix(")") else {
                        return Err(serde::de::Error::custom("expected `name(...)`"));
                    };
                    let Some((variant, data)) = description.split_once('(') else {
                        return Err(serde::de::Error::custom("expected `name(...)`"));
                    };
                    match variant {
                        "POISON" => Ok(SymbolWrapper(Symbol::Poison(
                            data.parse::<u8>().map_err(serde::de::Error::custom)?,
                        ))),
                        _ => Err(serde::de::Error::custom(format!(
                            "expected a known name, found `{variant}`"
                        ))),
                    }
                }
            }
        }
    }
    

    Playground.

    Instead of SymbolWrapper, you can use Symbol directly and have the custom deserialization logic everywhere it is a field with #[serde(deserialize_with = "...")].