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
}

person Eric    schedule 06.09.2015    source источник
comment
Почему это не должно быть возможно? Просто проанализируйте его как fold по результату, чтобы преобразовать его в [Person]? Но я никогда не использовал aeson. Может быть, более четко сформулируйте, в чем проблема.   -  person jakubdaniel    schedule 06.09.2015


Ответы (3)


Вы не можете определить экземпляр для [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
person mniip    schedule 06.09.2015

Если вы включите 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)
person Alexander VoidEx Ruchkin    schedule 06.09.2015
comment
Разве это не приводит к противоречивым экземплярам, ​​если, скажем, мы должны были создать экземпляр FromJSON Person - person mniip; 06.09.2015

ответ mniip преобразует JSON Object в Map, что приводит к отсортированному по идентификатору списку результатов. Если вам не нужны результаты, отсортированные таким образом, вероятно, лучше использовать более прямой подход для ускорения процесса. В частности, 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

Обратите внимание, что, как и в ответе Александра VoidEx Ручкина, это последовательно выполняет преобразование из PersonInfo в Person явно внутри монады Parser. Поэтому было бы легко изменить его, чтобы он выдавал ошибку синтаксического анализа, если Person не проходит какую-либо проверку высокого уровня. Ответ Александра также демонстрирует полезность комбинатора withObject, который я бы использовал, если бы знал, что он существует.

person dfeuer    schedule 06.09.2015