Haskell使用动态JSON字段建模类型的方法?

时间:2014-11-02 00:05:40

标签: json haskell aeson

我是Haskell的新手,来自命令式编程背景。我希望能够以“Haskell方式”将对象序列化为JSON,但不太确定如何做到这一点。

我已经阅读了Chapter 5 of RealWorldHaskell,其中讨论了JSON,并与Aeson一起玩。我还查看了一些用Haskell编写的JSON API库,例如:

这让我能够从对象创建非常基本的JSON字符串(也感谢this blog post):

{-# LANGUAGE OverloadedStrings, DeriveGeneric #-}

import Data.Aeson
import GHC.Generics

data User = User {
  email :: String,
  name :: String
} deriving (Show, Generic)

instance ToJSON User

main = do
  let user = User "foo@example.com" "Hello World"
  let json = encode user
  putStrLn $ show json

那将打印出来:

"{\"email\":\"foo@example.com",\"name\":\"Hello World\"}"

现在的目标是,将另一个字段添加到可以包含任意字段的User实例。 Facebook Graph API有一个名为data的字段,它是一个JSON对象,包含您想要的任何属性。例如,你可以向Facebook的API发出这样的请求(伪代码,不完全熟悉Facebook API):

POST api.facebook.com/actions
{
  "name": "read",
  "object": "book",
  "data": {
    "favoriteChapter": 10,
    "hardcover": true
  }
}

前两个字段nameobjectString类型,而data字段是任意属性的地图。

问题是,在上面User模型中实现这一目标的“Haskell方式”是什么?

我可以理解如何做这个简单的案例:

data User = User {
  email :: String,
  name :: String,
  data :: CustomData
} deriving (Show, Generic)

data CustomData = CustomData {
  favoriteColor :: String
}

但这并不是我想要的。这意味着User类型在序列化为JSON时将始终如下所示:

{
  "email": "",
  "name": "",
  "data": {
    "favoriteColor": ""
  }
}

问题是,你如何制作它,所以你只需要定义User类型一次,然后可以将任意字段附加到data属性,同时仍然可以从静态类型中受益(或者接近它的任何东西,还不熟悉类型的细节)。

2 个答案:

答案 0 :(得分:4)

这取决于您对任意数据的含义。我将提取我认为“数据包含任意文档类型”的合理且非平凡的定义,并向您展示几种可能性。

首先,我会指出我过去的一篇博文。这演示了如何解析结构或性质不同的文档。此处的现有示例:http://bitemyapp.com/posts/2014-04-17-parsing-nondeterministic-data-with-aeson-and-sum-types.html

应用于您的数据类型时,这可能类似于:

data CustomData = NotesData Text | UserAge Int deriving (Show, Generic)
newtype Email = Email Text deriving (Show, Generic)
newtype Name  = Name  Text deriving (Show, Generic)

data User = User {
  email :: Email,
  name  :: Name,
  data  :: CustomData
} deriving (Show, Generic)

接下来,我将向您展示使用更高的kinded类型定义可参数化的结构。此处的现有示例:http://bitemyapp.com/posts/2014-04-11-aeson-and-user-created-types.html

newtype Email = Email Text deriving (Show, Generic)
newtype Name  = Name  Text deriving (Show, Generic)

-- 'a' needs to implement ToJSON/FromJSON as appropriate
data User a = User {
  email :: Email,
  name  :: Name,
  data  :: a
} deriving (Show, Generic)

使用上面的代码,我们已经参数化data并使User成为更高级的类型。现在User已根据其类型参数的类型进行参数化。 data字段现在可以是文档,例如User CustomData,字符串User Text或数字User Int。您可能想要一个语义上有意义的类型,而不是Int / String。必要时使用newtype来完成此任务。

关于如何将数据类型的结构和含义借给许多其他编码为(Double,Double)的数据类型,请参阅https://github.com/NICTA/coordinate

如果您认为合适,可以将这些方法结合起来。这部分取决于您是否希望您的类型能够在封闭文档的类型参数中表达特定的,单一的可能性。

我有大量的JSON处理代码以及如何在https://github.com/bitemyapp/bloodhound

的库中构建数据的示例

指导原则是尽可能通过类型使无效数据无法代表。当单独的类型无法验证您的数据时,请考虑使用“智能构造函数”。

在此处详细了解智能构造函数:https://www.haskell.org/haskellwiki/Smart_constructors

答案 1 :(得分:2)

如果你真的想接受Aeson的FromJSON类完全随意的JSON子结构,我建议你创建一个字段user :: Value,它是Aeson的通用类型。 JSON值。 如果您稍后发现此JSON值的可能类型,您可以再次使用FromJSON进行转换,但最初它将保留那里的任何内容。