Aeson:将动态密钥解析为类型字段

时间:2015-09-06 09:07:31

标签: haskell aeson

让我们说有一个类似的JSON:

{
  "bob_id" : {
    "name": "bob",
    "age" : 20
  },
  "jack_id" : {
    "name": "jack",
    "age" : 25
  }
}

是否可以将其解析为[Person] Person定义如下?

data Person = Person {
   id   :: Text
  ,name :: Text
  ,age  :: Int
}

3 个答案:

答案 0 :(得分:7)

您无法从字面上定义[Person]的实例,因为aeson已经包含[a]的实例,但您可以创建一个新类型,并为其提供实例。

Aeson还包含实例FromJSON a => FromJSON (Map Text a),这意味着如果aeson知道如何解析某些内容,它就知道如何解析该内容的字典。

您可以定义类似于dict中的值的临时数据类型,然后使用Map实例定义FromJSON PersonList,其中newtype PersonList = PersonList [Person]

data PersonInfo = PersonInfo { infoName :: Text, infoAge :: Int }

instance FromJSON PersonInfo where
    parseJSON (Object v) = PersonInfo <$> v .: "name" <*> v .: "age"
    parseJSON _ = mzero

data Person = Person { id :: Text, name :: Text, age :: Int }
newtype PersonList = PersonList [Person]

instance FromJSON PersonList where
    parseJSON v = fmap (PersonList . map (\(id, PersonInfo name age) -> Person id name age) . M.toList) $ parseJSON v

答案 1 :(得分:3)

如果您启用FlexibleInstances,则可以为[Person]制作实例。您可以将对象解析为Map Text Value,然后解析map中的每个元素:

{-# LANGUAGE UnicodeSyntax, OverloadedStrings, FlexibleInstances #-}

module Person (
    ) where

import Data.Aeson
import Data.Aeson.Types
import Data.Text.Lazy
import Data.Text.Lazy.Encoding
import Data.Map (Map)
import qualified Data.Map as M

data Person = Person {
    id ∷ Text,
    name ∷ Text,
    age ∷ Int }
        deriving (Eq, Ord, Read, Show)

instance FromJSON [Person] where
    parseJSON v = do
        objs ← parseJSON v ∷ Parser (Map Text Value)
        sequence [withObject "person"
            (\v' → Person i <$> v' .: "name" <*> v' .: "age") obj | 
            (i, obj) ← M.toList objs]

test ∷ Text
test = "{\"bob_id\":{\"name\":\"bob\",\"age\":20},\"jack_id\":{\"name\":\"jack\",\"age\":25}}"

res ∷ Maybe [Person]
res = decode (encodeUtf8 test)

答案 2 :(得分:2)

mniip's answer将JSON Object转换为Map,这会导致按ID排序的结果列表。如果您不需要以这种方式排序结果,那么使用更直接的方法来加快速度可能会更好。特别是,Object实际上只是HashMap Text Value,因此我们可以使用HashMap操作来处理它。

请注意,我将id字段重命名为ident,因为大多数Haskell程序员会假设id引用Prelude中的标识函数或更一般的标识Control.Category中的箭头。

module Aes where
import Control.Applicative
import Data.Aeson
import Data.Text (Text)
import qualified Data.HashMap.Strict as HMS

data PersonInfo = PersonInfo { infoName :: Text, infoAge :: Int }

instance FromJSON PersonInfo where
-- Use mniip's definition here

data Person = Person { ident :: Text, name :: Text, age :: Int }

newtype PersonList = PersonList [Person]

instance FromJSON PersonList where
  parseJSON (Object v) = PersonList <$> HMS.foldrWithKey go (pure []) v
    where
      go i x r = (\(PersonInfo nm ag) rest -> Person i nm ag : rest) <$>
                     parseJSON x <*> r
  parseJSON _ = empty

请注意,与Alexander VoidEx Ruchkin's answer一样,这会在PersonInfo monad中明确地将Person转换为Parser。因此,如果Person未通过某种高级验证,则很容易修改它以产生解析错误。亚历山大的答案也证明了withObject组合子的实用性,如果我知道它已经存在,我会使用它。