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.
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}`"
))),
}
}
}
}
}
Instead of SymbolWrapper
, you can use Symbol
directly and have the custom deserialization logic everywhere it is a field with #[serde(deserialize_with = "...")]
.