Aeson: parsing dynamic keys as type field

 ̄綄美尐妖づ 提交于 2019-12-10 09:34:47

问题


Let's say there is a JSON like:

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

Is it possible to parse it to [Person] with Person defined like below?

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

回答1:


You cannot define an instance for [Person] literally, because aeson already includes an instance for [a], however you can create a newtype, and provide an instance for that.

Aeson also includes the instance FromJSON a => FromJSON (Map Text a), which means if aeson knows how to parse something, it knows how to parse a dict of that something.

You can define a temporary datatype resembling a value in the dict, then use the Map instance to define FromJSON PersonList, where 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



回答2:


If you enable FlexibleInstances, you can make instance for [Person]. You can parse your object to Map Text Value and then parse each element in 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)



回答3:


mniip's answer converts the JSON Object to a Map, which leads to a result list sorted by ID. If you don't need the results sorted in that fashion, it's probably better to use a more direct approach to speed things up. In particular, an Object is really just a HashMap Text Value, so we can use HashMap operations to work with it.

Note that I renamed the id field to ident, because most Haskell programmers will assume that id refers to the identity function in Prelude or to the more general identity arrow in 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

Note that, like Alexander VoidEx Ruchkin's answer, this sequences the conversion from PersonInfo to Person explicitly within the Parser monad. It would therefore be easy to modify it to produce a parse error if the Person fails some sort of high-level validation. Alexander's answer also demonstrates the utility of the withObject combinator, which I'd have used if I'd known it existed.



来源:https://stackoverflow.com/questions/32421836/aeson-parsing-dynamic-keys-as-type-field

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!