Multiple possible types for a serializable struct's field

Viewed 687

I'm writing a simple bookmark manager which will save data in a JSON format like this;

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

I've tried to write children field to allow two possible types (Directory and Bookmark) by creating a common Entry trait but I'm hitting a wall as I can't implement Serialize trait from serde for the Entry 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 {}

Is it even possible to make this work or I should create a different structure which wouldn't contain a field with multiple possible values? I was thinking about loading the JSON as HashMap<String, serde_json::Value> and looping through the hash map but I was wondering if there is some more elegant way to do this.

1 Answers

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(),
        }
    }
}
Related