serde:根据另一个字段的值反序列化一个字段。

6
我想将一个类似于JSON的数据格式反序列化为下面的Data结构,但我无法编写对应Rust类型的serde Deserialize实现。
{ "type": "TypeA", "value": { "id": "blah", "content": "0xa1b.." } }

enum Content {
   TypeA(Vec<u8>),
   TypeB(BigInt), 
}

struct Value {
    id: String,
    content: Content,
}

struct Data {
   typ: String,
   value: Value,
}

选取正确的Content枚举值是困难的,这要基于typ值判断。据我所知serde中的反序列化是无状态的,因此无法:

  • 知道在反序列化content时typ的值是什么(尽管反序列化的顺序已经保证)
  • 或将typ值注入到反序列化程序然后再收集它

使用Serde如何达到这个效果?

我看了以下内容:

  • serde_state,但我无法使宏起作用,而且这个库又包装了serde,这让我担心
  • DeserializeSeed,但我的理解是它必须替代Deserialize用于所有类型,而我的数据模型很大

现有的SO答案通常利用相关字段处于相同级别的事实,但这在这里不适用:实际数据模型庞大、深入,并且字段之间“相距甚远”。


数据来自外部来源吗? - Netwave
所有的 Content 变体都保证是相同的吗?如果不是,你的实际格式是否更加复杂?因为如果不是,那么手动为 Data 实现反序列化可能是一个简单的解决方案。 - vallentin
@vallentin 我不确定你所说的“相同”是什么意思。在实际模型中,有十几个(固定)变体。在真实模型中,typcontent之间有3个嵌套级别。 - Bruno Grieder
@BrunoGrieder,你可以实现自定义反序列化。这应该不难。只需将其反序列化为中间的Json::Value,从中提取所需内容并构建项目即可。 - Netwave
@BrunoGrieder,请提供更好的[mcve],您不能说“在实际模型中”并期望我们猜测它。请提供代码和输入,带有assert_eq!以显示您想要的内容。不要省略您认为微不足道的细节。 - Stargateur
显示剩余9条评论
3个回答

8
使用标记会更加简单,但需要改变你的数据结构。
use serde::{Deserialize, Deserializer}; // 1.0.130
use serde_json; // 1.0.67

#[derive(Debug, Deserialize)]
#[serde(tag = "type", content = "value")]
enum Data {
   TypeA(Value<String>),
   TypeB(Value<u32>), 
}

#[derive(Debug, Deserialize)]
struct Value<T> {
    id: String,
    content: T,
}



fn main() {
    let input = r#"{"type": "TypeA", "value": { "id": "blah", "content": "0xa1b..."}}"#;
    let data: Data = serde_json::from_str(input).unwrap();
    println!("{:?}", data);
}

游乐场

此外,你可以使用一些中间件 serde_json::Value 编写自己的自定义反序列化器:
use serde::{Deserialize, Deserializer};// 1.0.130
use serde_json; // 1.0.67

#[derive(Debug)]
enum Content {
   TypeA(String),
   TypeB(String), 
}

#[derive(Debug)]
struct Value {
    id: String,
    content: Content,
}

#[derive(Debug)]
struct Data {
   typ: String,
   value: Value,
}


impl<'de> Deserialize<'de> for Data {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let json: serde_json::value::Value = serde_json::value::Value::deserialize(deserializer)?;
        let typ = json.get("type").expect("type").as_str().unwrap();
        let value = json.get("value").expect("value");
        
        let id = value.get("id").expect("id").as_str().unwrap();
        let content = value.get("content").expect("content").as_str().unwrap();
        
        Ok(Data {
            typ: typ.to_string(),
            value: Value {
                id: id.to_string(),
                content: {
                    match typ {
                        "TypeA" => Content::TypeA(content.to_string()),
                        "TypeB" => Content::TypeB(content.to_string()),
                        _ => panic!("Invalid type, but this should be an error not a panic"),
                    }
                }
            }
        })    
    }
}

fn main() {
    let input = r#"{"type": "TypeA", "value": { "id": "blah", "content": "0xa1b..."}}"#;
    let data: Data = serde_json::from_str(input).unwrap();
    println!("{:?}", data);
}

游乐场

免责声明:我没有正确处理错误,您还可以将匹配内容提取到一个函数中。上述代码仅用于说明主要思想。


现在我对我的解决方案感到有些傻,没有想到可以反过来做。唯一的缺点是如果你想要带字段的变体。虽然使用单独的结构体比使用我的解决方案更容易,但这需要一些手动的类型检查 :) - vallentin
谢谢。我正在审查标记,并将回复。使用值对象(作为缓存)时,我有一些问题,主要问题是将包含两个相关字段(此处为“Data”)的对象完全描述以便在其“Deserialize”实现中实例化-而我的对象很大。 - Bruno Grieder
标记似乎在相邻标记中运作良好,但在其他情况下很难使其工作,因此需要对模型进行修改。在我的情况下不是一个选项。 - Bruno Grieder

6

有几种不同的方法可以解决这个问题,例如使用自定义的impl Deserialize for Data,然后将其反序列化为serde_json::Value,再手动在类型之间进行转换。

有一个相对应的示例,可以参考我之前写的回答。它不是一一对应的解决方案,但可能会给出手动实现Deserialize的提示,以满足你的需求。


话虽如此,个人认为当必须手动impl Deserialize时,最好尽量减少并反序列化到另一个类型中,然后使用#[serde(from = "FromType")]进行自动转换。

首先,建议我们引入enum ContentType,而不是type_: String

#[derive(Deserialize, Clone, Copy, Debug)]
enum ContentType {
    TypeA,
    TypeB,
    TypeC,
    TypeD,
}

现在,让我们考虑您介绍的类型。我已经向Content添加了一些额外的变体,因为您提到变体可能不同。
#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum Content {
    TypeA(Vec<u8>),
    TypeB(Vec<u8>),
    TypeC(String),
    TypeD { foo: i32, bar: i32 },
}

#[derive(Deserialize, Debug)]
struct Value {
    id: String,
    content: Content,
}

#[derive(Deserialize, Debug)]
#[serde(try_from = "IntermediateData")]
struct Data {
    #[serde(alias = "type")]
    type_: ContentType,
    value: Value,
}

目前还没有太疯狂或者差别很大的内容。所有的“魔法”都在 IntermediateData 类型中发生,以及 impl TryFrom


首先,我们引入一个check_type()函数,它接受一个ContentType并将其与Content进行比较。如果Content变体与ContentType变体不匹配,则将其转换。

简而言之,在使用#[serde(untagged)]时,当Serde尝试反序列化Content时,如果可能,它将始终返回第一个成功的变体,如果有的话。所以如果它可以反序列化一个Vec<u8>,那么它将始终得到Content::TypeA()。知道了这一点,那么在我们的check_type()中,如果ContentTypeTypeB,并且ContentTypeA,那么我们就将其简单地更改为TypeB

impl Content {
    // TODO: impl proper error type instead of `String`
    fn check_type(self, type_: ContentType) -> Result<Self, String> {
        match (type_, self) {
            (ContentType::TypeA, content @ Self::TypeA(_)) => Ok(content),
            (ContentType::TypeB, Self::TypeA(content)) => Ok(Self::TypeB(content)),
            (ContentType::TypeC | ContentType::TypeD, content) => Ok(content),
            (type_, content) => Err(format!(
                "unexpected combination of {:?} and {:?}",
                type_, content
            )),
        }
    }
}

现在我们需要中间数据IntermediateData和一个TryFrom转换,该转换会在Content上调用check_type()
#[derive(Deserialize, Debug)]
struct IntermediateData {
    #[serde(alias = "type")]
    type_: ContentType,
    value: Value,
}

impl TryFrom<IntermediateData> for Data {
    // TODO: impl proper error type instead of `String`
    type Error = String;

    fn try_from(mut data: IntermediateData) -> Result<Self, Self::Error> {
        data.value.content = data.value.content.check_type(data.type_)?;

        Ok(Data {
            type_: data.type_,
            value: data.value,
        })
    }
}

就这些。现在我们可以根据以下内容来测试它:

// serde = { version = "1", features = ["derive"] }
// serde_json = "1.0"

use std::convert::TryFrom;

use serde::Deserialize;

// ... all the previous code ...

fn main() {
    let json = r#"{ "type": "TypeA", "value": { "id": "foo", "content": [0, 1, 2, 3] } }"#;
    let data: Data = serde_json::from_str(json).unwrap();
    println!("{:#?}", data);

    let json = r#"{ "type": "TypeB", "value": { "id": "foo", "content": [0, 1, 2, 3] } }"#;
    let data: Data = serde_json::from_str(json).unwrap();
    println!("{:#?}", data);

    let json = r#"{ "type": "TypeC", "value": { "id": "bar", "content": "foo" } }"#;
    let data: Data = serde_json::from_str(json).unwrap();
    println!("{:#?}", data);

    let json = r#"{ "type": "TypeD", "value": { "id": "baz", "content": { "foo": 1, "bar": 2 } } }"#;
    let data: Data = serde_json::from_str(json).unwrap();
    println!("{:#?}", data);
}

然后,它将正确生成带有 Content::TypeAContent::TypeBContent::TypeC 和最后一个 Content::TypeDData


最后,有一个issue #939,它讨论了添加 #[serde(validate = "...")] 的问题。然而,它是在2017年创建的,所以不要抱太大希望。


+1 感谢您提供详细的答案。这种自我纠正的方法虽然有些笨重,但很有趣,您可以将其反序列化为可修复的内容。我一定会尝试一下。我认为您甚至可以通过让serde反序列化为“Data”的实例,然后直接进行后续修复来简化它。 - Bruno Grieder
绝对有可能。但其中的一个缺点是,有人可能会忘记调用 "data.fix()"。尽管可以通过函数隐藏所有这些,并调用 Data::from_json(...) 而不是 serde_json::from_str::<Data>(...) :) - vallentin
经过测试各种答案,我认为这是最明智和高效的方法,即使不是理想的方法:让serde做它能做的事情,并后缀需要的内容。 - Bruno Grieder
如何读取value.content.foo或value.content[0]的值? - Ant
@Ant 使用 match value.content 并在 Value 变量上进行匹配。 - vallentin
谢谢@vallentin。#[serde(from = "FromType")]的建议非常好。它解决了我的问题。 - Vagelis Prokopiou

1
DeserializeSeed 可以与普通的 Deserialize 代码混合使用,不需要用于所有类型。在这里,只需使用它来反序列化 Value 即可。 Playground
use serde::de::{DeserializeSeed, IgnoredAny, MapAccess, Visitor};
use serde::*;
use std::fmt;

#[derive(Debug)]
enum ContentType {
    A,
    B,
    Unknown,
}

#[derive(Debug)]
enum Content {
    TypeA(String),
    TypeB(i32),
}

#[derive(Debug)]
struct Value {
    id: String,
    content: Content,
}

#[derive(Debug)]
struct Data {
    typ: String,
    value: Value,
}

impl<'de> Deserialize<'de> for Data {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct DataVisitor;

        impl<'de> Visitor<'de> for DataVisitor {
            type Value = Data;

            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                formatter.write_str("struct Data")
            }

            fn visit_map<A>(self, mut access: A) -> Result<Self::Value, A::Error>
            where
                A: MapAccess<'de>,
            {
                let mut typ = None;
                let mut value = None;

                while let Some(key) = access.next_key()? {
                    match key {
                        "type" => {
                            typ = Some(access.next_value()?);
                        }
                        "value" => {
                            let seed = match typ.as_deref() {
                                Some("TypeA") => ContentType::A,
                                Some("TypeB") => ContentType::B,
                                _ => ContentType::Unknown,
                            };
                            value = Some(access.next_value_seed(seed)?);
                        }
                        _ => {
                            access.next_value::<IgnoredAny>()?;
                        }
                    }
                }

                Ok(Data {
                    typ: typ.unwrap(),
                    value: value.unwrap(),
                })
            }
        }

        deserializer.deserialize_map(DataVisitor)
    }
}

impl<'de> DeserializeSeed<'de> for ContentType {
    type Value = Value;

    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct ValueVisitor(ContentType);

        impl<'de> Visitor<'de> for ValueVisitor {
            type Value = Value;

            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                formatter.write_str("struct Value")
            }

            fn visit_map<A>(self, mut access: A) -> Result<Self::Value, A::Error>
            where
                A: MapAccess<'de>,
            {
                let mut id = None;
                let mut content = None;

                while let Some(key) = access.next_key()? {
                    match key {
                        "id" => {
                            id = Some(access.next_value()?);
                        }
                        "content" => {
                            content = Some(match self.0 {
                                ContentType::A => Content::TypeA(access.next_value()?),
                                ContentType::B => Content::TypeB(access.next_value()?),
                                ContentType::Unknown => {
                                    panic!("Should not happen if type happens to occur before value, but JSON is unordered.");
                                }
                            });
                        }
                        _ => {
                            access.next_value::<IgnoredAny>()?;
                        }
                    }
                }

                Ok(Value {
                    id: id.unwrap(),
                    content: content.unwrap(),
                })
            }
        }

        deserializer.deserialize_map(ValueVisitor(self))
    }
}

fn main() {
    let j = r#"{"type": "TypeA", "value": {"id": "blah", "content": "0xa1b.."}}"#;
    dbg!(serde_json::from_str::<Data>(j).unwrap());
    let j = r#"{"type": "TypeB", "value": {"id": "blah", "content": 666}}"#;
    dbg!(serde_json::from_str::<Data>(j).unwrap());
    let j = r#"{"type": "TypeB", "value": {"id": "blah", "content": "Foobar"}}"#;
    dbg!(serde_json::from_str::<Data>(j).unwrap_err());
}

这种解决方案的主要缺点是你失去了推导代码的可能性。

+1 感谢您纠正这个问题。这就是我的意思,但不正确地表述了:您会失去推导的能力。很遗憾 DeserializeSeed 没有扩展 Deserialize - Bruno Grieder

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接