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
}
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
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)
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.