JSON in Haskell

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 examplei:

{ "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 Haskellii :

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
    showJSON gd = makeObj
        [ ("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:

    readJSON (JSObject obj) = let
            jsonObjAssoc = fromJSObject obj
        in do
            para <- mLookup "para" jsonObjAssoc >>= readJSON
            seeAlso <- mLookup "GlossSeeAlso" jsonObjAssoc >>= readJSON
            return $ GlossDef
                { glossDefPara = para
                , glossDefSeeAlso = seeAlso
                }
 
    readJSON _ = fail ""

The function mLookup is a wrapper around lookup that makes it a bit nicer to work with in monads other than Maybe:

mLookup a as = maybe (fail $ "No such element: " ++ a) return (lookup 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
    showJSON ge = makeObj
        [ ("ID", showJSON $ glossEntryId ge)
        , ("SortAs", showJSON $ glossEntrySortAs ge)
        , ("GlossDef", showJSON $ glossEntryGlossDef ge)
        ]
 
    readJSON (JSObject obj) = let
            jsonObjAssoc = fromJSObject obj
        in do
            id <- mLookup "ID" jsonObjAssoc >>= readJSON
            sortAs <- mLookup "SortAs" jsonObjAssoc >>= readJSON
            gd <- mLookup "GlossDef" jsonObjAssoc >>= readJSON
            return $ GlossEntry
                { glossEntryId = id
                , glossEntrySortAs = sortAs
                , glossEntryGlossDef = gd
                }

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 :-)

  1. it’s a modified version of what I found here.[back]
  2. The file should include {-# LANGUAGE DeriveDataTypeable #-} and both Data.Typeable and Data.Data must be imported.[back]
Share

5 Comments

  1. readJSON (JSObject obj) = GlossEntry <$> f "ID" <*> f "SortAs" <*> f "GlossDef"       where f x = mLookup x jsonObjAssoc >>= readJSON

  2. Haskell could really use a fast Json parser like Python’s json module (which wraps a C library that does the actual parsing). But that SYB-based module does look pretty easy to use.

  3. Pingback: Simple math and JSON in Haskell – (SDL game series part 4) | Animal-Machine.Com

  4. Pingback: therning.org/ magnus » Blog Archive » Revisiting JSON in Haskell

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>