问题
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