使用aeson解析JSON以获取复合数据类型

时间:2014-12-25 11:21:16

标签: json haskell aeson

我有以下数据类型:

data DocumentOrDirectory = Document DocumentName DocumentContent 
                         | Directory DirectoryName [DocumentOrDirectory]

我附带了以下forJSON代码。它有效,但需要改进。它应该单独转换文档和目录,但我不知道该怎么做。

instance JSON.ToJSON DocumentOrDirectory where
    toJSON (Document documentName documentContent) = JSON.object
        [ "document" JSON..= JSON.object 
            [ "name" JSON..= (T.pack $ id documentName)
            , "content" JSON..= (T.pack $ id documentContent)
            ]
        ]
    toJSON (Directory dirName dirContent) = JSON.object
        [ "directory" JSON..= JSON.object 
            [ "name" JSON..= (T.pack $ id dirName)
            , "content" JSON..= JSON.toJSON dirContent
            ]
        ]

我需要能够从JSON解析DocumentOrDirectory对象。这就是我提出的(不起作用):

instance JSON.FromJSON DocumentOrDirectory where
    parseJSON (Object v@(Document documentName documentContent)) = 
        DocumentOrDirectory <$> documentName .: "name"
                            <*> documentContent .: "content"
    parseJSON (Object v@(Directory dirName dirContent) = 
        DocumentOrDirectory <$> dirName .: "name"
                            <*> dirContent .: "content"
    parseJSON _ = mzero

如何修改现有代码以便能够将数据转换为JSON?

1 个答案:

答案 0 :(得分:4)

让我们一步一步地解决这个问题。

首先,我假设为了示例,名称和内容只是String

type DirectoryName = String
type DocumentName = String
type DocumentContent = String

您提到要分别序列化DocumentDirectory。也许你也希望单独使用它们。让我们将它们分开:

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

我们应该检查DocumentDirectory是如何序列化的(我对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实例。我们看到密钥(directorydocument)显示基础数据是Directory还是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"))])))

我们可以在对象数据中使用 type tag 序列化数据,然后序列化和反序列化看起来会更好一些:

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中我们更明确:liftDocumentliftDirectory可以被认为是显式类型强制/ upcast,如果你想考虑对象


编辑: the working code as gist