rust - 可序列化结构体字段的多种可能类型

标签 rust traits serde

我正在编写一个简单的书签管理器,它将以 JSON 格式保存数据,如下所示;

{
    "children": [
        {
            "name": "Social",
            "children": [
                {
                    "name": "Facebook",
                    "url": "https://facebook.com",
                    "faviconUrl": "",
                    "tags": [],
                    "keyword": "",
                    "createdAt": 8902351,
                    "modifiedAt": 90981235
                }
            ],
            "createdAt": 235123534,
            "modifiedAt": 23531235
        }
    ]
}

我尝试写children通过创建公共(public) Directory 字段允许两种可能的类型( BookmarkEntry )特质,但我遇到了困难,因为我无法实现 Serialize来自serde的特征对于Entry特质。

use serde::{Serialize, Deserialize, Serializer};

#[derive(Serialize, Deserialize)]
struct Root<'a> {
    children: Vec<&'a dyn Entry>,
}

#[derive(Serialize, Deserialize)]
struct Directory<'a> {
    name: String,
    created_at: u64,
    children: Vec<&'a dyn Entry>,
    modified_at: u64
}

#[derive(Serialize, Deserialize)]
struct Bookmark {
    name: String,
    url: String,
    favicon_url: String,
    tags: Vec<String>,
    keyword: String,
    created_at: u64,
    modified_at: u64,
}

trait Entry {
    fn is_directory(&self) -> bool;
}

impl Entry for Directory<'_> {
    fn is_directory(&self) -> bool {
        true
    }
}

impl Entry for Bookmark {
    fn is_directory(&self) -> bool {
        false
    }
}

// can't do this
impl Serialize for Entry {}

是否有可能实现这项工作,或者我应该创建一个不包含具有多个可能值的字段的不同结构?我正在考虑将 JSON 加载为 HashMap<String, serde_json::Value>并循环遍历 HashMap ,但我想知道是否有一些更优雅的方法来做到这一点。

最佳答案

如果您只是将 Entry 设为枚举而不是特征,并更改 &dyn Entry到只是Entry ,那么一切都应该正常工作,除非您最终会在 JSON 中得到一个额外的级别,以及一个额外的标记条目,告诉您该条目是什么类型。正如 Masklinn 在评论中指出的那样,这种情况也是不正确的,但可以使用 #[serde(rename_all = "camelCase")] 进行修复。 .

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Root {
    children: Vec<Entry>,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Directory {
    name: String,
    created_at: u64,
    children: Vec<Entry>,
    modified_at: u64,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Bookmark {
    name: String,
    url: String,
    favicon_url: String,
    tags: Vec<String>,
    keyword: String,
    created_at: u64,
    modified_at: u64,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
enum Entry {
    Directory(Directory),
    Bookmark(Bookmark),
}

如果您确实不需要额外的级别和标签,那么您可以使用 serde(untagged)注释 Entry .

#[derive(Deserialize, Serialize, Debug)]
#[serde(untagged)]
enum Entry {
    Directory(Directory),
    Bookmark(Bookmark),
}

如果您需要更多的灵活性,您可以创建一个中间结构 BookmarkOrDirectory包含两者的所有字段,其中仅出现在其中一个的字段为 Option然后执行TryFrom<BookmarkOrDirectory>对于 Entry并使用serde(try_from=...)serde(into=...)转换为适当的形式/从适当的形式转换。下面是一个示例实现。它可以编译,但有一些 todo!分散在其中,并使用String作为一种错误类型,这是很hacky的 - 当然未经测试。

use core::convert::TryFrom;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct Root {
    children: Vec<Entry>,
}

#[derive(Clone)]
struct Directory {
    name: String,
    created_at: u64,
    children: Vec<Entry>,
    modified_at: u64,
}

#[derive(Clone)]
struct Bookmark {
    name: String,
    url: String,
    favicon_url: String,
    tags: Vec<String>,
    keyword: String,
    created_at: u64,
    modified_at: u64,
}

#[derive(Serialize, Deserialize, Clone)]
#[serde(try_from = "BookmarkOrDirectory", into = "BookmarkOrDirectory")]
enum Entry {
    Directory(Directory),
    Bookmark(Bookmark),
}

#[derive(Serialize, Deserialize)]
struct BookmarkOrDirectory {
    name: String,
    url: Option<String>,
    favicon_url: Option<String>,
    tags: Option<Vec<String>>,
    keyword: Option<String>,
    created_at: u64,
    modified_at: u64,
    children: Option<Vec<Entry>>,
}

impl BookmarkOrDirectory {
    pub fn to_directory(self) -> Result<Directory, (Self, String)> {
        // Check all the fields are there
        if !self.children.is_some() {
            return Err((self, "children is not set".to_string()));
        }
        // TODO: Check extra fields are not there
        Ok(Directory {
            name: self.name,
            created_at: self.created_at,
            children: self.children.unwrap(),
            modified_at: self.modified_at,
        })
    }
    pub fn to_bookmark(self) -> Result<Bookmark, (Self, String)> {
        todo!()
    }
}

impl TryFrom<BookmarkOrDirectory> for Entry {
    type Error = String;
    fn try_from(v: BookmarkOrDirectory) -> Result<Self, String> {
        // Try to parse it as direcory
        match v.to_directory() {
            Ok(directory) => Ok(Entry::Directory(directory)),
            Err((v, mesg1)) => {
                // if that fails try to parse it as bookmark
                match v.to_bookmark() {
                    Ok(bookmark) => Ok(Entry::Bookmark(bookmark)),
                    Err((_v, mesg2)) => Err(format!("unable to convert to entry - not a bookmark since '{}', not a directory since '{}'", mesg2, mesg1))
                }
            }
        }
    }
}

impl Into<BookmarkOrDirectory> for Bookmark {
    fn into(self) -> BookmarkOrDirectory {
        todo!()
    }
}

impl Into<BookmarkOrDirectory> for Directory {
    fn into(self) -> BookmarkOrDirectory {
        todo!()
    }
}

impl Into<BookmarkOrDirectory> for Entry {
    fn into(self) -> BookmarkOrDirectory {
        match self {
            Entry::Bookmark(bookmark) => bookmark.into(),
            Entry::Directory(directory) => directory.into(),
        }
    }
}

关于rust - 可序列化结构体字段的多种可能类型,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/67594909/

相关文章:

csv - 如何在不复制的情况下在 Rust 中迭代 Vec 时分配切片?

scala - 在 Scala 中泛化构造函数

rust - 如何为 `IntoIter` 绑定(bind) `<&Self as IntoIterator>` 类型?

rust - 将对象转换为 serde_json::Value 无需序列化和反序列化

intellij-idea - Intellij 报告 Rust 工具链未安装,即使我刚刚安装了它

rust - 如何在编译过程中注释代码以生成工件?

macros - 错误 `cannot be named the same as a tuple variant` 是什么意思?

rust - 无法返回 Option<associated type> 因为关联类型未调整大小

serialization - 如何使用需要实现者实现 serde::Deserialize 的方法创建特征

rust - 序列化结构上的子属性似乎不起作用