Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Prevent a serialized struct in Rust from being deserialized into a different one

I want to prevent that data with the same attribute names, but different semantics cannot be accidentally loaded into one-another. For example:

use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize, Debug)]
#[serde(deny_unknown_fields)]
struct RubyVersionV1 {
    // Records explicit or default version we want to install
    version: String,
}

#[derive(Deserialize, Serialize, Debug)]
#[serde(deny_unknown_fields)]
struct RubyVersionV2 {
    // Programmer realized that there was ambiguity in their prior storage schema
    //
    // They want to decouple an explicit version, from the default case.
    // To make this change they switch to using an Option stores None
    // when a default is used, and Some when an explicit version is used.
    version: Option<String>,
}

// Both use a trait to deliver the information the program needs
trait RubyVersion {
    fn version_to_install(&self) -> String;
}

impl RubyVersion for RubyVersionV1 {
    fn version_to_install(&self) -> String {
        self.version.clone()
    }
}

const DEFAULT_VERSION: &str = "3.2.2";

impl RubyVersion for RubyVersionV2 {
    fn version_to_install(&self) -> String {
        self.version
            .clone()
            .unwrap_or_else(|| DEFAULT_VERSION.to_string())
    }
}

Runnable example:

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=94308306cf81e0fcaff06fb3a2457a6b

In this code, data serialized to disk from RubyVersionV1 could be accidentally deserialized as RubyVersionV2.

This is a trivial example that is trying to demonstrate the more complicated domain that I'm working on (PR https://github.com/heroku/buildpacks-ruby/pull/246). If I can guarantee that V1 can NEVER be deserialized to V2 (rather than requiring the programmer to be careful) then my users' lives will be much easier.

like image 836
Schneems Avatar asked Nov 28 '25 18:11

Schneems


1 Answers

You can do it by adding a void field to each struct to act as a version marker:

use serde::{ Serialize, Deserialize };

#[derive (Serialize, Deserialize, Debug, Default)]
struct V1 {
    text: String,
    version1: (),
}

#[derive (Serialize, Deserialize, Debug, Default)]
struct V2 {
    text: Option<String>,
    version2: (),
}

fn main() {
    let v1 = V1::default();
    let s1 = serde_json::to_string (&v1).unwrap();
    let v2: Result<V2, _> = serde_json::from_str (&s1);
    println!("{v1:?} -> {s1} -> {v2:?}");
}

Playground

like image 133
Jmb Avatar answered Dec 01 '25 15:12

Jmb



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!