Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to parse nested JSON with aeson

Tags:

json

haskell

I am trying to parse JSON of the following form using aeson

{"field":{"name":"..."}}

or

{"tag":{"name":"..."}}

or

{"line":{"number":"..."}}

to construct the following data type

data Rule = Line Integer
          | Field L.ByteString
          | Tag L.ByteString

Unfortunately, I face two problems that I've not found solutions to, namely:

  1. How do I parse nested JSON? Looking at the implementation of (.:), it uses lookup to extract a specific key's value. I'm hesitant to do something like this as it seems to be relying too much on the specifics of how aeson implements things. Am I wrong in thinking this is an issue?

  2. How do I use the correct data constructor based on which key is present in the JSON? All my efforts with <|> have led me nowhere.

I would post the code I've written thus far, but I haven't even gotten to the point where I have anything worth posting.

like image 630
lukerandall Avatar asked Jul 26 '11 07:07

lukerandall


2 Answers

How about the following?

{-# LANGUAGE OverloadedStrings #-}

import Control.Applicative
import           Data.Aeson
import           Data.Aeson.Types
import qualified Data.ByteString      as B
import qualified Data.ByteString.Lazy as L
import qualified Data.Map             as M

data Rule = Line Integer
          | Field L.ByteString
          | Tag L.ByteString
          deriving Show

instance FromJSON Rule where
  parseJSON j = do
    o <- parseJSON j -- takes care of JSON type check
    case M.toList (o :: Object) of
      [("field", Object o')] -> Field <$> o' .: "name"
      [("tag",   Object o')] -> Tag   <$> o' .: "name"
      [("line",  Object o')] -> Line  <$> o' .: "number"
      _                      -> fail "Rule: unexpected format"
like image 101
hvr Avatar answered Sep 24 '22 08:09

hvr


For this problem I created a helper function that looks up a key:

lookupE :: Value -> Text -> Either String Value
lookupE (Object obj) key = case H.lookup key obj of
        Nothing -> Left $ "key " ++ show key ++ " not present"
        Just v  -> Right v
loopkupE _ _             = Left $ "not an object"

and using it two functions that nest into objects:

(.:*) :: (FromJSON a) => Value -> [Text] -> Parser a
(.:*) value = parseJSON <=< foldM ((either fail return .) . lookupE) value

(.:?*) :: (FromJSON a) => Value -> [Text] -> Parser (Maybe a)
(.:?*) value = either (\_ -> return Nothing) (liftM Just . parseJSON)
                . foldM lookupE value
-- Or more simply using Control.Alternative.optional
-- (.:?*) value keys = optional $ value .:* keys

Only lookupE depends on the internal representation so it's easy to modify it, if that changes. Then {"tag":{"name":"..."}} is parsed as v .:* ["tag", "name"]. Note that it also works for empty lists - v .:* [] is equivalent to parseJSON v.

like image 26
Petr Avatar answered Sep 23 '22 08:09

Petr