可序列化结构字段的多种可能类型
Multiple possible types for a serializable struct's field
我正在编写一个简单的书签管理器,它将以这样的 JSON 格式保存数据;
{
"children": [
{
"name": "Social",
"children": [
{
"name": "Facebook",
"url": "https://facebook.com",
"faviconUrl": "",
"tags": [],
"keyword": "",
"createdAt": 8902351,
"modifiedAt": 90981235
}
],
"createdAt": 235123534,
"modifiedAt": 23531235
}
]
}
我试图通过创建一个共同的 Entry
特征来编写 children
字段以允许两种可能的类型(Directory
和 Bookmark
),但我正在打一堵墙,因为我无法从 serde
为 Entry
特征实现 Serialize
特征。
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>
并循环遍历哈希映射,但我想知道是否有更优雅的方法来执行此操作。
如果您只是将 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=...) and serde(into=...) 将 to/from 转换为适当的形式。下面是一个示例实现。它编译,但有一些 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(),
}
}
}
我正在编写一个简单的书签管理器,它将以这样的 JSON 格式保存数据;
{
"children": [
{
"name": "Social",
"children": [
{
"name": "Facebook",
"url": "https://facebook.com",
"faviconUrl": "",
"tags": [],
"keyword": "",
"createdAt": 8902351,
"modifiedAt": 90981235
}
],
"createdAt": 235123534,
"modifiedAt": 23531235
}
]
}
我试图通过创建一个共同的 Entry
特征来编写 children
字段以允许两种可能的类型(Directory
和 Bookmark
),但我正在打一堵墙,因为我无法从 serde
为 Entry
特征实现 Serialize
特征。
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>
并循环遍历哈希映射,但我想知道是否有更优雅的方法来执行此操作。
如果您只是将 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=...) and serde(into=...) 将 to/from 转换为适当的形式。下面是一个示例实现。它编译,但有一些 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(),
}
}
}