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

Aeson: parsing dynamic keys as type field

假设有一个 JSON 比如:

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

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

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

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

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

你可以在字典中定义一个类似值的临时数据类型,然后使用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

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

{-# 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)

将 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

请注意,与 一样,这会在 Parser monad 中明确地对从 PersonInfoPerson 的转换进行排序。因此,如果 Person 未能通过某种高级验证,则很容易对其进行修改以产生解析错误。 Alexander 的回答还展示了 withObject 组合器的实用性,如果我知道它存在,我会使用它。