【问题标题】:Serde struct version checkSerde 结构版本检查
【发布时间】:2021-12-15 15:22:10
【问题描述】:

我想在我的结构中添加一个简单的版本方案 + 检查:

#[derive(Serialize, Deserialize)]
struct Versioned {
    version: u32,
    other_field: String,
}

impl Versioned {
    const FORMAT_VERSION: u32 = 42;
}

这个想法相当简单:version 字段被反序列化(如果不存在,则为 0*),如果不等于 FORMAT_VERSION,则会引发错误。然后,处理结构的其余部分。这里的顺序很重要:否则,反序列化错误可能不正确(您可能会得到类似“缺少字段new_field”而不是“版本太旧”)。

我尝试了一些关于包装器类型和自定义反序列化器的想法,但都没有提供良好的用户体验。到目前为止我最好的:

#[derive(Serialize, Deserialize)]
struct Payload {
    some_field: String,
}

#[derive(Serialize, Deserialize)]
#[serde(tag = "version")]
enum Versioned {
    #[serde(rename = "0")]
    Inner(Payload),
}

它使用枚举包装器进行版本检查,并使用两个From 实现来轻松进行类型转换。缺点是:

  • 该字段*没有默认值(= 不向后兼容未版本化的数据)
  • 旧版本上的错误消息(“错误:未知变体 42,预期 0 在第 2 行第 17 列”而不是“无效版本”之类的内容)

* 注意:向后兼容性要求使事情变得更加困难。所以请记住,请务必立即为您的所有数据添加 version 字段!

【问题讨论】:

  • @JeroenVervaeke 因为我 90% 的用例都是 JSON 序列化,这很好,尽管我更喜欢更通用的解决方案。 (不过取决于自描述数据格式也可以)

标签: rust serde


【解决方案1】:

您可以通过为version 字段编写自定义反序列化函数来实现这两点。您可以通过将#[serde(default)] 放在字段上来使其成为可选。 自定义反序列化函数可以具有您想要检查版本号并生成所需错误消息的任何逻辑。

#[derive(Serialize, Deserialize, Debug)]
struct Versioned {
    #[serde(deserialize_with = "ensure_version", default)]
    version: u32,
    other_field: String,
}

impl Versioned {
    const FORMAT_VERSION: u32 = 42;
}

fn ensure_version<'de, D: Deserializer<'de>>(d: D) -> Result<u32, D::Error> {
    let version = u32::deserialize(d)?;
    if version != Versioned::FORMAT_VERSION {
        return Err(D::Error::custom("Version mismatch for Versioned"));
    }
    Ok(version)
}

Playground

【讨论】:

  • 如果 OP 放弃此要求,这将是一种方法:“如果它不等于 FORMAT_VERSION,则会引发错误。然后,处理结构的其余部分."。这里我们不保证版本会在 other_field 之前被验证。
  • 如果version 字段在序列化形式中首先出现,它也将首先被处理。字段之间的顺序可能是一个问题,例如,如果您更改字段的类型。但是使用上面的代码你永远不会得到一个缺少字段的错误,因为该字段总是在Versioned 检查缺少的字段之前处理。如果您希望始终先处理 version 字段,则需要使用外部标记的枚举之类的东西。来自 OP 的带有相邻标记的枚举的“解决方案”没有实现这一点。
  • 我希望看到一个版本,它使用我的答案中的未标记枚举技巧来确保正确的序列化。类似#[serde(flatten)] payload: MaybePayload 然后#[serde(untagged)] enum MaybePayload { Payload(Payload), Invalid(serde_json::Value)}。然后,根据version的值,使用某种类型转换方法来转换错误。
【解决方案2】:

我的第一种方法是尝试构建一个自定义反序列化器,它首先反序列化为一个新结构(VersionedModel,它派生Deserialize),只有version 字段。这不起作用,因为VersionedModel::deserialize(deserializer) 使用了反序列化器,因此您无法反序列化任何其他数据。

我的第二种方法是使用Deserializer&lt;'de&gt; 上的一种方法,但是,所有方法都会消耗反序列化器,因此您无法先读取版本,然后再读取其他字段。

第三种(也是最幼稚的方法,不是很干净,但很有效)是分两步实现反序列化,如下所示:

use serde::{Deserialize, Serialize};
use thiserror::Error;

#[derive(Deserialize, Serialize)]
struct Versioned {
    version: u32,
    other_field: String,
}

impl Versioned {
    const FORMAT_VERSION: u32 = 42;
}

#[derive(Debug, Error)]
pub enum DeserializeError {
    #[error("Version field is missing")]
    VersionIsMissing,
    #[error("Invalid version, got: {actual:}, expected: {expected:}")]
    InvalidVersion { actual: u32, expected: u32 },
    #[error(transparent)]
    DeserializeError(#[from] serde_json::Error),
}

fn deserialize_model(value: &str) -> Result<Versioned, DeserializeError> {
    #[derive(Deserialize, Serialize)]
    struct VersionOnly {
        version: u32,
    }

    let VersionOnly { version } =
        serde_json::from_str(&value).map_err(|_| DeserializeError::VersionIsMissing)?;

    if version == Versioned::FORMAT_VERSION {
        Ok(serde_json::from_str(&value)?)
    } else {
        Err(DeserializeError::InvalidVersion {
            actual: version,
            expected: Versioned::FORMAT_VERSION,
        })
    }
}

Playground

最后一种方法:编写自己的序列化程序,编写自己的访问者(基本上复制粘贴所有生成的宏代码并添加一些更改)。我会推荐这种方法,因为它很容易出错并且可能不值得麻烦。

【讨论】:

    【解决方案3】:

    感谢所有其他答案的灵感!另请参阅 https://github.com/serde-rs/serde/issues/1799https://github.com/serde-rs/serde/issues/912#issuecomment-981657911 了解有关我正在使用的未标记枚举技巧的更多详细信息。

    基本上,需要两个步骤来根据需要改进问题中的代码:

    1. #[serde(other)] Unsupported 变体添加到版本化枚举中
    2. 将枚举包装在另一个处理默认版本的未标记枚举中(称为Legacy
    use serde::{Deserialize, Serialize};
    
    #[derive(Serialize, Deserialize, PartialEq, Debug)]
    pub struct Data {
        field: String,
    }
    
    #[derive(Debug, PartialEq, Serialize, Deserialize)]
    #[serde(tag = "version")]
    pub enum VersionChecked {
        #[serde(rename = "0")]
        V0(Data),
        #[serde(other)]
        Unsupported,
    }
    
    #[derive(Debug, PartialEq, Serialize, Deserialize)]
    #[serde(untagged)]
    pub enum DefaultVersion {
        Versioned(VersionChecked),
        Legacy(Data),
        // Optional, if you want to "catch" the serde error
        // Invalid(serde_json::Value),
    }
    
    fn main() {
        dbg!(serde_json::from_str::<DefaultVersion>(r#"{ "version": 0, "field": "Somestring" }"#).unwrap());
        dbg!(serde_json::from_str::<DefaultVersion>(r#"{ "field": "Somestring" }"#).unwrap());
        dbg!(serde_json::from_str::<DefaultVersion>(r#"{ "blablah": "blablah" }"#));
        dbg!(serde_json::from_str::<DefaultVersion>(r#"{ "version": 42, "foo": "bar" }"#));
    }
    

    Playground

    仍然可以在使用上进行一些改进。我在想TryFrom&lt;DefaultVersion&gt; for Data 用于一个方向,一些serialize_as 用于另一个方向。但现在可以了。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-07-12
      • 1970-01-01
      • 1970-01-01
      • 2022-01-11
      • 1970-01-01
      相关资源
      最近更新 更多