1
votes

Cosider the following JSON structure:

{"k1":
  {"k2":
    [{"a": 3, "b": 4, "c": 2},
     {"a": 1, "b": 2, "c": 9}]},
 "irrelevant": "x"}

and Haskell data type:

data My = My Int Int

The above JSON should be parsed to a List of My: [My], whereas the two Int should each be taken from the "a" and "b" key of the JSON array:

[My 3 4, My 1 2]

Admittedly I'm already facing troubles with the simplest parts of it.

Here's how I started using Aeson:

import           Data.Aeson
import qualified Data.ByteString.Lazy.Char8 as L8

sample :: L8.ByteString
sample = "{\"k1\":{\"k2\":[{\"a\": 3, \"b\": 4, \"c\": 2}, {\"a\": 1, \"b\": 2, \"c\": 9}]}, \"irrelevant\": \"x\"} "

At the repl:

decode sample :: Maybe Object
Just (Object (fromList [("irreleva...

This works as expected, the JSON is parsed. However the next step, getting the Object at key "k1", does not work:

:t (fromJust $ (decode sample :: Maybe Object)) .: "k1"
...
  :: FromJSON a => aeson-0.11.2.1:Data.Aeson.Types.Internal.Parser a

I'm receiving a Parser a type here, I'd need/expect getting another Object or Maybe Object at this point.

Am I on the right path?

2

2 Answers

0
votes

I'm going to start from the end then get back to your questions.

Solving With Class

Typically you make a Haskell data type for each of your JSON types and write FromJSON classes that implement the parser. You don't have to, but it does lighten the mental load and fall inline with what you might observe in other project. To that end lets make just a couple types My for your elements and Mys for a list of these elements:

{-# LANGUAGE OverloadedStrings #-}
import           Data.Aeson
import qualified Data.ByteString.Lazy.Char8 as L8
import qualified Data.Vector as V

sample :: L8.ByteString
sample = "{\"k1\":{\"k2\":[{\"a\": 3, \"b\": 4, \"c\": 2}, {\"a\": 1, \"b\": 2, \"c\": 9}]}, \"irrelevant\": \"x\"} "

newtype Mys = Mys [My]
    deriving (Eq,Ord,Show)
data My = My Int Int
    deriving (Eq,Ord,Show)

Ok, no issues. Now we can extract from your k1 record a list of the a-b-c objects and run the My parser on these objects to get just the a and b values:

instance FromJSON Mys where
  parseJSON (Object v) = do
   do k1Val <- v .: "k1"
      case k1Val of
        Object k1 ->
          do k2Val <- k1 .: "k2"
             Mys . V.toList <$> mapM parseJSON k2Val
        _         -> fail "k1 was not an Object"
  parseJSON o = fail $ "Invalid type for Mys: " ++ show o

That is, to parse a Mys we need an object, the object must have a k1 entry that is another object. k1 must have a k2 entry which we can parse as a Vector of My values.

instance FromJSON My where
  parseJSON (Object v) = My <$> v .: "a" <*> v .: "b"
  parseJSON o = fail $ "Invalid type for My: " ++ show o

And the My data is just a parsing of a and b fields as Int. Behold:

> decode sample :: Maybe Mys
Just (Mys [My 3 4,My 1 2])

Without Class

You asked about :t (fromJust $ (decode sample :: Maybe Object)) .: "k1", which is just a fancy way of asking about the type of .::

> :t (.:)
(.:) :: FromJSON a => Object -> Text -> Parser a

So you are providing an Object and Text to get a Parser, as you said. I wouldn't advise using the Parser monad again - you effectively just used it for decode. In short I'd say no, you weren't on a path to happiness.

If you aren't going to use the API as designed then just forget the combinators and use the data types directly. That is, lots of case destruction of the Value types. First is k1 which is an Object (just a HashMap) then extract the k2 value which is an Array (a Vector), finally for each element of the Vector you extract out an Object again and lookup the a and b keys there. I was going to write it up for example, but it is exceedingly ugly if you don't at least allow yourself a Maybe monad.

0
votes

The tutorial 'Aeson: the tutorial' by Artyom may help you, as it helped me.

Following it, I arrived at the following code. It extends sample to allow the processing of different samples (some with a variety of defects) to be examined. main expects the identity of the sample that is to be processed to be supplied as the first argument to the compiled executable.

Starting at the bottom of the JSON structure, and working upwards, function parseMy :: Value -> Parser My processes an object with 'a' 'b' keys to yield a My (if successful); intermediate helper function parseMyList' processes an array of such objects to yield a list of My; and parseMyList processes an object with key 'k1', yielding in turn an object with key 'k2', to also yield a list of My.

In main, parse applies parseMyList :: Value -> Parser [My] to the result of decode (if it was successful).

{-# LANGUAGE OverloadedStrings #-}

module Main (main) where

import           Data.Aeson ((.:), decode, Value)
import           Data.Aeson.Types (parse, Parser, withArray, withObject)
import qualified Data.ByteString.Lazy.Char8 as L8 (ByteString)
import qualified Data.Vector as V (toList)
import           System.Environment (getArgs)

data My = My Int Int deriving (Show)

sample :: String -> L8.ByteString
sample "1" = "{\"k1\":{\"k2\":[{\"a\": 3, \"b\": 4, \"c\": 2}, {\"a\": 1, \"b\": 2, \"c\": 9}]}, \"irrelevant\": \"x\"} "
sample "2" = "{\"k1\":{\"k2\":[{\"a\": 3, \"b\": 4, \"c\": 2}, {\"a\": 1, \"c\": 9}]}, \"irrelevant\": \"x\"} "
sample "3" = "{\"k1\":{\"k3\":[{\"a\": 3, \"b\": 4, \"c\": 2}, {\"a\": 1, \"b\": 2, \"c\": 9}]}, \"irrelevant\": \"x\"} "
sample "4" = "{\"k1\":{\"k2\":[{\"a\": 3, \"b\": 4, \"c\": 2}]}, \"irrelevant\": \"x\"} "
sample _   = "Error"

parseMy :: Value -> Parser My
parseMy = withObject "object" $ \o -> do
    a <- o .: "a"
    b <- o .: "b"
    return $ My a b

parseMyList' :: Value -> Parser [My]
parseMyList' = withArray "array" $ \arr ->
    mapM parseMy (V.toList arr)

parseMyList :: Value -> Parser [My]
parseMyList = withObject "object" $ \o -> do
    k1 <- o .: "k1"
    k2 <- k1 .: "k2"
    parseMyList' k2 

main :: IO ()
main = do
    args <- getArgs
    case args of
        []  -> fail "expected sample identity as the first argument"
        n:_ -> do
            putStrLn $ "Processing sample " ++ n
            case decode (sample n) of
                Just result -> print $ parse parseMyList result
                Nothing     -> fail "decoding failed"