【问题标题】:Multiple possible types for a serializable struct's field可序列化结构字段的多种可能类型
【发布时间】:2021-05-18 22:49:58
【问题描述】:

我正在编写一个简单的书签管理器,它将像这样以 JSON 格式保存数据;

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

我尝试编写 children 字段以允许两种可能的类型(DirectoryBookmark),方法是创建一个常见的 Entry 特征,但我碰壁了,因为我无法实现 @987654326 @ trait 来自 serdeEntry trait。

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&lt;String, serde_json::Value&gt; 并循环遍历哈希映射,但我想知道是否有更优雅的方法来做到这一点。

【问题讨论】:

    标签: rust traits serde


    【解决方案1】:

    如果您只是将条目设置为枚举而不是特征,并将 &amp;dyn Entry 更改为只是 Entry,那么一切都应该正常工作,除非您最终会在 JSON 中增加一个级别,以及一个额外的标记条目告诉您条目是什么类型。正如 Masklinn 在 cmets 中指出的那样,这种情况也是不正确的,但可以使用 #[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,然后为Entry 实现TryFrom&lt;BookmarkOrDirectory&gt; 和使用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(),
            }
        }
    }
    

    【讨论】:

    • 您实际上不需要做任何事情,serde 支持使用the serde(untagged) annotation 对枚举进行“结构”匹配。演示:play.rust-lang.org/…
    • 我所说的“任何东西”是指所有复杂的东西,包括一个额外的结构和一堆转换。相反,将Entry 标记为serde(untagged) 就足够了,在DirectoryBookmark 上派生SerializeDeserialize,并将两者都标记为serde(rename_all = "camelCase")
    • untagged 的一个缺点是it can be pretty slow
    • @Masklinn 谢谢,我不知道它存在 --- 我现在已将该建议纳入正文。
    猜你喜欢
    • 1970-01-01
    • 2018-11-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-03-16
    • 2023-03-09
    相关资源
    最近更新 更多