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"]
2 Answers 2
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
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.
FromJSON
instances. This will make your code way cleaner. Could you also provide a sample JSON string that you want to parse? \$\endgroup\$