3
\$\begingroup\$

I'm trying to reduce nested JSON data using Haskell and Aeson without creating data/record types.
The following code works, but seems ugly and unreadable.
Any advice on cleaning this up would be great.

Thank you

import GHC.Exts
import Data.Maybe (fromJust)
import Data.Text (Text)
import Data.Aeson
import qualified Data.HashMap.Strict as M

testVal :: Value
testVal = Object $ fromList [
  ("items", Array $ fromList [ 
    (Object $ fromList [
      ("entity", (Object $ fromList [ 
        ("uuid", String "needed-value1")]))]),
    (Object $ fromList [
      ("entity", (Object $ fromList [ 
        ("uuid", String "needed-value2")]))])
    ])]

getItems :: Value -> Value
getItems (Object o) = fromJust $ M.lookup "items" o

getEntities :: Value -> Value
getEntities (Array a) = Array $ fmap (\(Object o) -> fromJust $ M.lookup "entity" o) a

getUuids :: Value -> Value
getUuids (Array a) = Array $ fmap (\(Object o) -> fromJust $ M.lookup "uuid" o) a

getTexts :: Value -> [Text]
getTexts (Array a) = toList $ fmap (\(String s) -> s) a


someFunc :: IO ()
someFunc = do
  let items = getItems testVal
      entities = getEntities items
      uuids = getUuids entities
      texts = getTexts uuids
  print texts

output:

["needed-value1","needed-value2"]
\$\endgroup\$
1
  • 1
    \$\begingroup\$ You should really use an algebraic data type to implement your JSON interface. Then you can automatically derive FromJSON instances. This will make your code way cleaner. Could you also provide a sample JSON string that you want to parse? \$\endgroup\$
    – Erich
    Apr 15 '20 at 10:46
1
\$\begingroup\$

You should strongly consider @Erich's advice and define datatypes but it is possible to write quick and dirty queris without them.

{-# LANGUAGE OverloadedStrings #-}                                                                    
{-# LANGUAGE QuasiQuotes #-}

import Data.Text (Text)
import Data.Aeson (Value, Result(..), decode, withObject, (.:))
import Data.Aeson.Types (parse)
import Data.Aeson.QQ.Simple
import Control.Monad ((>=>))


testVal :: Value
testVal = [aesonQQ|
  {"items": [
    {"entity": {"uuid": "needed-value1"}},
    {"entity": {"uuid": "needed-value2"}}
  ]}
|]

getUuids :: Value -> Result [Text]
getUuids = parse $ withObject ""
  $ (.: "items") >=> mapM ((.: "entity") >=> (.: "uuid"))


main :: IO ()
main = do
  print $ getUuids testVal
  print $ getUuids <$> decode "{}"
  print $ getUuids <$> decode "{"

Another option is to use lens-aeson package:

import Control.Lens
import Data.Aeson.Lens (values, key, _String)

-- [...]

main :: IO ()
main = print
  $ testVal ^.. key "items" . values . key "entity" . key "uuid" . _String
\$\endgroup\$
2
\$\begingroup\$

You should really use an algebraic data type to implement your JSON interface. You can even automatically derive FromJSON instances with DeriveGeneric GHC extension, e.g.

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}

import GHC.Generics
import Data.Aeson
import Data.ByteString.Lazy (ByteString)

data Query = Query { items :: [Entity] }
             deriving (Show, Generic)

instance FromJSON Query -- automatically derived instance by DeriveGeneric

data Entity = Entity { uuid :: String }
              deriving (Show)

instance FromJSON Entity where
    parseJSON = withObject "Entity" $ \v -> do
        -- as your json data is additionally nested with an entity object
        -- extract the entity object first
        obj <- v .: "entity"
        -- then extract the uuid field from the entity object
        uid <- obj .: "uuid"
        return $ Entity uid

testVal :: ByteString
testVal = "{\"items\": [{\"entity\": {\"uuid\": \"needed-value1\"}}, {\"entity\": {\"uuid\": \"needed-value2\"}}]}"

main :: IO ()
main = do
    let mayQuery = decode testVal
    case mayQuery of
        Just query -> print $ map uuid $ items query
        Nothing -> putStrLn "JSON parsing error"

I transformed your sample Value into a JSON string, to make the parsing clearer.

The FromJSON instance for Query is automatically derived, if you want to write it by hand, you can do this in analogy to the FromJSON instance of Entity.

This way of parsing your JSON data is very scalable, as you can easily add new fields to your data types, without complicating your code.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.