让我们一步一步地解决这个问题。
首先,我假设名称和内容只是String
:
type DirectoryName = String
type DocumentName = String
type DocumentContent = String
你提到你想序列化Document
和Directory
分开。也许您也想单独使用它们。让我们将它们分成不同的类型:
data Document = Document DocumentName DocumentContent deriving Show
data Directory = Directory DirectoryName [DocumentOrDirectory] deriving Show
newtype DocumentOrDirectory = DocumentOrDirectory (Either Document Directory) deriving Show
现在DocumentOrDirectory
是类型别名或Either Document Directory
. 我们使用newtype
,因为我们想为它编写自己的实例。默认Either
实例对我们不起作用。
让我们定义一些辅助函数:
liftDocument :: Document -> DocumentOrDirectory
liftDocument = DocumentOrDirectory . Left
liftDirectory :: Directory -> DocumentOrDirectory
liftDirectory = DocumentOrDirectory . Right
有了这个定义,我们可以编写单独的ToJSON
实例:
instance ToJSON Document where
toJSON (Document name content) = object [ "document" .= object [
"name" .= name,
"content" .= content ]]
instance ToJSON Directory where
toJSON (Directory name content) = object [ "directory" .= object [
"name" .= name,
"content" .= content ]]
instance ToJSON DocumentOrDirectory where
toJSON (DocumentOrDirectory (Left d)) = toJSON d
toJSON (DocumentOrDirectory (Right d)) = toJSON d
我们应该检查如何Document
和Directory
被序列化(我美化了 JSON 输出):
*Main> let document = Document "docname" "lorem"
*Main> B.putStr (encode document)
{
"document": {
"content": "lorem",
"name": "docname"
}
}
*Main> let directory = Directory "dirname" [Left document, Left document]
*Main> B.putStr (encode directory) >> putChar '\n'
{
"directory": {
"content": [
{
"document": {
"content": "lorem",
"name": "docname"
}
},
{
"document": {
"content": "lorem",
"name": "docname"
}
}
],
"name": "directory"
}
}
结果B.putStr (encode $ liftDirectory directory)
是一样的!
下一步是编写解码器、FromJSON
实例。我们看到键 ( directory
or document
) 显示底层数据是Directory
or Document
。因此 JSON 格式是不重叠的(明确的),所以我们可以尝试解析Document
然后Directory
.
instance FromJSON Document where
parseJSON (Object v) = maybe mzero parser $ HashMap.lookup "document" v
where parser (Object v') = Document <$> v' .: "name"
<*> v' .: "content"
parser _ = mzero
parseJSON _ = mzero
instance FromJSON Directory where
parseJSON (Object v) = maybe mzero parser $ HashMap.lookup "directory" v
where parser (Object v') = Directory <$> v' .: "name"
<*> v' .: "content"
parser _ = mzero
parseJSON _ = mzero
instance FromJSON DocumentOrDirectory where
parseJSON json = (liftDocument <$> parseJSON json) <|> (liftDirectory <$> parseJSON json)
和检查:
*Main> decode $ encode directory :: Maybe DocumentOrDirectory
Just (DocumentOrDirectory (Right (Directory "directory" [DocumentOrDirectory (Left (Document "docname" "lorem")),DocumentOrDirectory (Left (Document "docname" "lorem"))])))
我们可以在对象数据中使用类型标签序列化数据,然后序列化和反序列化看起来会更好一些:
instance ToJSON Document where
toJSON (Document name content) = object [
"type" .= ("document" :: Text),
"name" .= name,
"content" .= content ]
生成的文档将是:
{
"type": "document",
"name": "docname",
"content": "lorem"
}
和解码:
instance FromJSON Document where
-- We could have guard here
parseJSON (Object v) = Document <$> v .: "name"
<*> v .= "content"
instance FromJSON DocumentOrDirectory where
-- Here we check the type, and dynamically select appropriate subparser
parseJSON (Object v) = do typ <- v .= "type"
case typ of
"document" -> liftDocument $ parseJSON v
"directory" -> liftDirectory $ parseJSON v
_ -> mzero
在具有子类型的语言中,您可以拥有这样的 scala:
sealed trait DocumentOrDirectory
case class Document(name: String, content: String) extends DocumentOrDirectory
case class Directory(name: String, content: Seq[DocumentOrDirectory]) extends DocumentOrDirectory
有人可能会争辩说这种方法(依赖于子类型)更方便。在 Haskell 中,我们更加明确:如果您喜欢考虑对象liftDocument
,liftDirectory
可以将其视为明确的类型强制/向上转换。
编辑: 作为要点的工作代码