JSON in Haskell
- Magnus Therning
The other day I wanted to experiment a bit with the JSON interface to AUR. Of course my first stop was at HackageDB to look for a Haskell package for parsing JSON. There are several of them, but only one that seemed suitable for some quick experimentation, especially I wanted to avoid pre-defining data types for the objects in the JSON interface. That failed however and I ended up switching to Python. It did bother me though, and later on, when I had some more time I decided to have another look at json. I was also helped by Don’s recent work on wrapping up the AUR JSON interface in Haskell.
After some searching online I found a reasonably good example1:
{ "ID": "SGML"
, "SortAs": "SGML"
, "GlossDef":
{ "para": "A meta-markup language, used to create markup languages such as DocBook."
, "GlossSeeAlso": ["GML", "XML"]
}
}
As a slight aside, the absolutely easiest way to add JSON to your program is to derive Data
(and by implication Typeable
too). This is the way I might have represented the data above in Haskell2:
data GlossDef = GlossDef
glossDefPara :: String
{ glossDefSeeAlso :: [String]
,deriving (Eq, Show, Typeable, Data)
}
data GlossEntry = GlossEntry
glossEntryId :: String
{ glossEntrySortAs :: String
, glossEntryGlossDef :: GlossDef
,deriving (Eq, Show, Typeable, Data) }
After that it’s as easy as using Text.JSON.Generic.toJSON
followed by Text.JSON.encode
:
> let gd = GlossDef "foo" ["bar", "baz"]
> let ge = GlossEntry "aa" "bb" gd
> putStrLn $ encode $ toJSON ge
{"glossEntryId":"aa","glossEntrySortAs":"bb","glossEntryGlossDef":{"glossDefPara":"foo","glossDefSeeAlso":["bar","baz"]}}
As can be seen the “names” of the members are derived from the field names in the datatypes. Great for when you are designing new JSON objects, not when you are writing code to parse an already existing object. For that there is another, more verbose way to do it.
Start with the same data types, but without deriving Typeable
and Data
:
data GlossDef = GlossDef
glossDefPara :: String
{ glossDefSeeAlso :: [String]
,deriving (Eq, Show)
}
data GlossEntry = GlossEntry
glossEntryId :: String
{ glossEntrySortAs :: String
, glossEntryGlossDef :: GlossDef
,deriving (Eq, Show) }
Then you have to implement Text.JSON.JSON
. Only two of the four functions must be implemented, showJSON
and readJSON
. Starting with GlossDef
:
instance JSON GlossDef where
= makeObj
showJSON gd "para", showJSON $ glossDefPara gd)
[ ("GlossSeeAlso", showJSON $ glossDefSeeAlso gd)
, ( ]
Basically this part defers to the already supplied implementations for the fields’ types. The same approach works for readJSON
too:
JSObject obj) = let
readJSON (= fromJSObject obj
jsonObjAssoc in do
<- mLookup "para" jsonObjAssoc >>= readJSON
para <- mLookup "GlossSeeAlso" jsonObjAssoc >>= readJSON
seeAlso return $ GlossDef
= para
{ glossDefPara = seeAlso
, glossDefSeeAlso
}
= fail "" readJSON _
The function mLookup
is a wrapper around lookup
that makes it a bit nicer to work with in monads other than Maybe
:
= maybe (fail $ "No such element: " ++ a) return (lookup a as) mLookup a as
(The choice to include the key in the string passed to fail
limits the usefulness somewhat in the general case, but for this example it doesn’t make any difference.)
Implementing the interface for GlossEntry
is analogous:
instance JSON GlossEntry where
= makeObj
showJSON ge "ID", showJSON $ glossEntryId ge)
[ ("SortAs", showJSON $ glossEntrySortAs ge)
, ("GlossDef", showJSON $ glossEntryGlossDef ge)
, (
]
JSObject obj) = let
readJSON (= fromJSObject obj
jsonObjAssoc in do
id <- mLookup "ID" jsonObjAssoc >>= readJSON
<- mLookup "SortAs" jsonObjAssoc >>= readJSON
sortAs <- mLookup "GlossDef" jsonObjAssoc >>= readJSON
gd return $ GlossEntry
= id
{ glossEntryId = sortAs
, glossEntrySortAs = gd
, glossEntryGlossDef }
With the JSON object mentioned at the top in the file test.json
the following is possible:
> f <- readFile "test.json"
> let (Ok j) = decode f :: Result GlossEntry
> putStrLn $ encode j
{"ID":"SGML","SortAs":"SGML","GlossDef":{"para":"A meta-markup language, used to create markup languages such as DocBook.","GlossSeeAlso":["GML","XML"]}}
I have a feeling the implemention of readJSON
could be simplified by using an applicative style, but I leave that as an excercise for the reader :-)