If you just make the Entry an enum rather than a trait, and change the &dyn Entry to just Entry, then everything should just work, except you will end up with one extra level in your JSON, and an additional tag entry telling you what type the entry is. As Masklinn pointed out in the comments, the case is also incorrect, but can be fixed using #[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),
}
If you really don't want the extra level and the tag, then you can use the serde(untagged) annotation to Entry.
#[derive(Deserialize, Serialize, Debug)]
#[serde(untagged)]
enum Entry {
Directory(Directory),
Bookmark(Bookmark),
}
If you need a bit more flexibility you can create an intermediate struct BookmarkOrDirectory that contains all the fields of both, with the fields that only occur in one as Option and then implement TryFrom<BookmarkOrDirectory> for Entry and use serde(try_from=...) and serde(into=...) to convert to/from the appropriate form. An example implementation is below. It compiles, but has a few todo! scattered in it, and uses String as an error type, which is hacky - and is of course untested.
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(),
}
}
}