luispauloml
luispauloml

Reputation: 1010

Parsing nested arrays and objects with Aeson

I am having problems writing a FromJSON instance to parse a JSON file containing an array of objects nested within another array of objects. The file is of the form:

[{"family":[{"name":"Jane","age":31,}
           ,{"name":"Ana","age":15,}]}
,{"family":[{"name":"Julia","age":45}
           ,{"name":"Chris","age":47}]}]

For that I created two new types:

-- A person
data Person = Person { name :: String
                     , age  :: Int} deriving (Show, Eq)

-- A family is a list of list of Person
newtype Family = Family {unFamily :: [[Person]]} deriving (Show, Eq)

And their instances are these (using XOverloadedStrings, aeson and text):

instance ToJSON Person where
  toJSON (Person n a) = object [ "name" .= n
                               , "age"  .= a]
instance FromJSON Person where
  parseJSON (Object o) = Person
    <$> o .:? "name" .!= ""
    <*> o .:? "age"  .!= 0
  parseJSON _ = mzero

instance ToJSON Family where
  toJSON (Family c) = toJSON . map (\v -> object $ ("family",v):[]) 
                             $ map toJSON c

I can make some tests like (with bytestring and maybe loaded)

> julia = Person "Julia" 45
> (== julia) . fromMaybe (Person "" 0) . decode . encode $ julia
True
> BS.putStrLn . BL.toStrict . encode $ Family [[julia,julia],[julia]]
[{"family":[{"age":45,"name":"Julia"},{"age":45,"name":"Julia"}]},{"family":[{"age":45,"name":"Julia"}]}]

and everything works as I wanted. But as I said, I couldn't write FromJSON Family, so I can't test like decode . encode as I did to julia.

I looked at the many types involved in aeson trying to find a way to make it work, but I simply couldn't do it. I could at least write toJSON for ToJSON Family because I learned that type Pair = (Text, Value).

Can anyone point me in a direction here? And is there a better way to write ToJSON Family?

Upvotes: 1

Views: 836

Answers (2)

Daniel Wagner
Daniel Wagner

Reputation: 153182

I would write

newtype Family = Family {unFamily :: [Person]}

instead. Then

instance FromJSON Family where
    parseJSON (Object o) = Family <$> (o .: "family")
    parseJSON v = typeMismatch "family" v
instance ToJSON Family where
    toJSON (Family c) = "family" .= c

should do the trick for the instances, and the FromJSON/ToJSON instances you get for free for [Family] will decode the example JSON you showed us/encode the example family you showed us as you want.

Upvotes: 2

willeM_ Van Onsem
willeM_ Van Onsem

Reputation: 477607

You can make a function that parses a sublist, and then use mapM to convert the sublists:

import Data.Foldable(toList)

instance FromJSON Family where
    parseJSON (Array arr) = Family <$> mapM parseSubList (toList arr)
        where parseSubList (Object o) = o .:? "family" .!= []

For example:

Prelude Data.Aeson Data.Foldable> decode "[{\"family\":[{\"age\":45,\"name\":\"Julia\"},{\"age\":45,\"name\":\"Julia\"}]},{\"family\":[{\"age\":45,\"name\":\"Julia\"}]}]" :: Maybe Family
Just (Family {unFamily = [[Person {name = "Julia", age = 45},Person {name = "Julia", age = 45}],[Person {name = "Julia", age = 45}]]})

Upvotes: 1

Related Questions