haskellaeson

Deserializing JSON of the form key - value pairs using aeson


I'm having trouble parsing JSON text in the form

{
"Person1": {
"name": "John Doe",
"job" : "Accountant",
"id": 123
},
"Person2": {
"name": "Jane Doe",
"job" : "Secretary",
"id": 321
}}

I define a new data type as follows

data Person = Person
{ name :: String
, job  :: String
, id   :: Int } deriving Show

But am having trouble defining an instance of FromJSON in this case.

Now if the JSON text were in the form of

{
 "People": [
    {
    "name": "John Doe",
    "job": "Accountant",
    "id": 123
    },
    {
    "name": "Jane Doe",
    "job": "Secretary",
    "id": 321
    }]
}

I could write

instance FromJSON person where
    parseJSON = withObject "Person" $ \obj -> Person
    <$> obj .: "name"
    <*> obj .: "job"
    <*> obj .: "id"

But I don't know how to translate this pattern to the other format.


Solution

  • A JSON dictionary, such as { "foo" : "bar" } can be decoded into Maps such as Map from the containers package or HashMap from unordered-containers. Once you have your FromJSON instance for Person then just use Aeson.decode to get a Map Text Person. If you'd like to get [Person] then you could further use Data.Map.elems.

    For example:

    #!/usr/bin/env cabal
    {-# LANGUAGE DeriveAnyClass #-}
    {-# LANGUAGE DeriveGeneric #-}
    {- cabal:
    build-depends: base, aeson, bytestring, containers
    -}
    
    import GHC.Generics
    import qualified Data.Aeson as Aeson
    import Data.Map (Map)
    import Data.Maybe (fromMaybe)
    import qualified Data.ByteString.Lazy.Char8 as BS
    
    data Person = Person 
            { name :: String
            , job  :: String
            , id   :: Int
            } deriving (Show, Generic, Aeson.ToJSON, Aeson.FromJSON)
    
    main :: IO ()
    main = print . decodePersons . BS.pack =<< getContents
    
    decodePersons :: BS.ByteString -> Map String Person
    decodePersons = fromMaybe (error "fail") . Aeson.decode
    

    The interesting bit is that Aeson.decode is returning a type Map String Person. Everything is is just there to make a complete demonstration.

    You can test this with the input:

    % ./so.hs <<EOF
    {
    "Person1": {
    "name": "John Doe",
    "job" : "Accountant",
    "id": 123
    },
    "Person2": {
    "name": "Jane Doe",
    "job" : "Secretary",
    "id": 321
    }}
    EOF
    

    To see output of:

    fromList [("Person1",Person {name = "John Doe", job = "Accountant", id = 123}),("Person2",Person {name = "Jane Doe", job = "Secretary", id = 321})]
    

    If you want to drop the Person1 and Person2 tags then just get the elements of the Map using Data.Map.elems.