Serde conditional renaming

I think that what I'm trying to do is impossible, but wanted to get a second opinion.

I'm introducing Rust to the codebase at work, and I need to mirror our existing behaviour written in Python. I have a number of situations similar to the following, a struct populated from a json in our database:

#[derive(Serialize, Deserialize)]
struct Thing {
    foo: String,
    baz: String,
    // ... 20 more fields
    version: u16,
}

In our database, until thing.version >= 22, the baz field was named bar. I need to be able to serialize and deserialize both older and newer records. What is the best way to do that?

As far as I can tell, serialize_with allows modifying only the value, not the key. And the rename attribute doesn't seem to allow conditionally renaming.

My current solution is a custom deserializer which deserializes to serde_json::Value, then check the version, and rename bar to baz if needed (and similarly for serialization). The problem is that my custom deserializer defines fn deserialize, so I can no longer derive Deserialize due to the name clash. I don't want to manually write a deserializer for all the many fields in the struct, so I'm maintaining a ThingHelper struct, identical to Thing, deserialize my modified serde_json::Value into a ThingHelper, then construct a Thing from the ThingHelper. This a lot of boilerplate, and doesn't help in the already uphill battle to convince my company to adopt more Rust.

Is there a better approach? E.g. if there is some way to both derive a serializer, and also implement my own (on the same struct), and call the derived from my own, that would be great. If that isn't possible, does that sounds like an appropriate feature request?

1 Like

I would wrap both fields in an Option and have a getter bar that either returns the value of the bar field if its version is greater or equal to 22 or the one from baz otherwise.

3 Likes

It depends. If the only field changing is baz, then I would do it with an enum that can be either bar or baz:

#[derive(Serialize, Deserialize)]
struct Thing {
    foo: String,
    #[serde(flatten)]
    bar_or_baz: BarOrBaz,
    // ... 20 more fields
    version: u16,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
enum BarOrBaz {
    Bar(String),
    Baz(String)
}

But if other fields are changing as well, then I would turn Thing into an enum and handle the multiple representations at that level.

3 Likes

Yes, marking both fields as optional is another approach. Though I would instruct serde to stop serializing if any of them is None, to keep the database records consistent:

#[serde(skip_serializing_if = "Option::is_none")]
bar: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
baz: Option<String>
2 Likes

Thank you both!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.