2

Yesod 包含Entity数据类型,即具有来自数据库的 id 的模型。Yesod 还创建Entity了 AesonToJSON类的实例,因此可以轻松地将其序列化为 json。更棒的是,Entity它可以被包裹在任何结构中,并且它也会被序列化。ToJSON支持协议的类型很多。它非常方便,我非常喜欢它。

不幸的是,Yesod 提供的序列化格式Entity不符合我的需求,我正在寻找一种简单而透明的方式来改变它。

这是一个例子。我有简单的模型

data Company = Company
  { companyName :: Text
  }

相应的实体将是

Entity CompanyId Company

现在,从数据库中获取实体并将其返回为 json 的代码看起来像

getCompanyR = do

    -- fetch companies from database
    -- `companies` contains list of `Entity CompanyId Company`
    companies <- runDB $ selectList ([] :: [Filter Company]) []

    -- return it as json
    -- List is also an instance of `ToJSON` so it could be serialized too
    return . toJSON $ companies

序列化列表看起来像

[{"key":"o52553881f14995dade000000","value":{"name":"Pizza World"}}]

我希望它是

[{"id":"o52553881f14995dade000000","name":"Pizza World"}]

我可以看到几个关于如何更改它的选项,每个选项都有其缺点:

  1. 根据我的格式做一个序列化的函数Entity,但是之后就无法序列化Listies了Entity。我将结束编写多个函数以Entity在它恰好属于的任何结构中进行序列化。

  2. 为 an 创建一个新类型Entity,但是我应该在序列化之前将所有Entityies 转换为ies。MyNewEntity这对我来说似乎很难看,它会导致不必要的转换噪音。

总而言之,我的问题是我无法更改Entity ToJSON实现,也无法让 Yesod 返回不同于Entity. 我被迫进行转换,但最透明的方法是什么?

4

1 回答 1

1

当您知道 Haskell 的类型类很好时,您将永远只有一个实例。但有时您需要将相同的结构序列化为不同的表示形式。这正是您遇到的问题。

我可以提出下一个解决方案:使用两个参数创建类型类(需要MultiParamTypeClasses扩展)。其中之一是您要序列化的结构;第二个将是一个标签来选择特定的 json 格式。例子:

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}

import Data.Aeson
import qualified Data.Vector as Vector
import Data.Text (Text)
import qualified Data.ByteString.Lazy as BSL

-- our custom variant on ToJSON
class ToJSON' tag a where
  toJSON' :: tag -> a -> Value

-- instance for lists, requires FlexibleInstances
instance ToJSON' tag a => ToJSON' tag [a] where
  toJSON' tag l = Array $ Vector.fromList $ map (toJSON' tag) l

-- our data type
data Test = Test {
  testString :: Text,
  testBool :: Bool
  }

-- the tag for the first json format
data TestToJSON1 = TestToJSON1

-- the first json format definition
instance ToJSON' TestToJSON1 Test where
  toJSON' _ test = object [
    "string1" .= String (testString test),
    "bool1" .= Bool (testBool test)
    ]

-- the tag for the second json format
data TestToJSON2 = TestToJSON2

-- the second json format definition
instance ToJSON' TestToJSON2 Test where
  toJSON' _ test = object [
    "string2" .= String (testString test),
    "bool2" .= Bool (testBool test)
    ]

-- usage example
main :: IO ()
main = do
  let test = Test {
    testString = "hello",
    testBool = False
    }
  BSL.putStr $ encode $ toJSON' TestToJSON1 test
  putStrLn ""
  BSL.putStr $ encode $ toJSON' TestToJSON1 [test, test]
  putStrLn ""
  BSL.putStr $ encode $ toJSON' TestToJSON2 test
  putStrLn ""
  BSL.putStr $ encode $ toJSON' TestToJSON2 [test, test]
  putStrLn ""

输出:

{"string1":"hello","bool1":false}
[{"string1":"hello","bool1":false},{"string1":"hello","bool1":false}]
{"bool2":false,"string2":"hello"}
[{"bool2":false,"string2":"hello"},{"bool2":false,"string2":"hello"}]

这样,您需要ToJSON'为每种数据类型为每种 json 格式定义一个实例,并为每个容器定义一个实例(在示例中,我只为列表实现了它)

如果你不喜欢MultiParamTypeClasses,你可以传递给toJSON'一个知道如何序列化你的数据类型的函数。

注意:OverloadedStrings不是绝对必要的。FlexibleInstances里面已经使用了Data.Aeson

于 2013-10-09T21:17:35.207 回答