sevo
sevo

Reputation: 4609

How to inspect parsed Aeson Value?

How can I browse large Aeson Values? I know there should be a string I'm interested in nested somewhere in the structure. How can I find it?

So far I only know how to query constructor and found out it's an Array. How can I dig deeper than that?

> take 20 $ show bt
"Array (fromList [Obj"

Upvotes: 2

Views: 466

Answers (1)

danidiaz
danidiaz

Reputation: 27766

The lens package has useful functions for inspecting tree-like structures like JSON Values. There's also the lens-aeson package with extra JSON-specific functions.

import Data.Text
import Data.Aeson
import Data.Aeson.Lens (_Value,_String) -- this is from lens-aeson
import Data.Foldable (toList)

import Control.Lens (Fold,folding,universeOf,toListOf,paraOf,preview)

We can begin by defining a lens Fold that extracts the immediate child Values of a given JSON Value:

vchildren :: Fold Value Value
vchildren = folding $ \v -> case v of
    Object o -> toList o
    Array a -> toList a
    _ -> []

folding is a function from lens that creates a Fold out a function that returns a list. A list of Values, in our case.

We can combine vchildren with the universeOf function from Control.Lens.Plated to get a function that extracts all the transitive descendants of a Value, including itself:

allValues :: Value -> [Value]
allValues = universeOf vchildren

And this function extracts all the texts contained in a Value. It uses the _String prism from Data.Aeson.Lens (a Prism is a bit like a "reified" pattern that can be passed around):

allTexts :: Value -> [Text]
allTexts = toListOf (folding allValues . _String)

Control.Lens.Plated also has interesting functions like paraOf, that let you build "paramorphims". A paramorphism is a "controlled destruction" of a tree-like structure starting from the leaves, and building the results upward. For example, this function

vpara :: (Value -> [r] -> r) -> Value -> r
vpara = paraOf vchildren

takes as its first parameter another function that receives the "current node" along with the intermediate results for the nodes below, and builds the intermediate result for the current node.

vpara will start consuming the JSON value from the leaves (the intermediate result list for those nodes is simply []) and proceeds upwards.

One possible use of vpara is obtaining the list of paths in the JSON that end in a text that matches some condition, like this:

type Path = [Value]

pathsThatEndInText :: (Text -> Bool) -> Value -> [Path]
pathsThatEndInText pred = vpara func
  where
    func :: Value -> [[Path]] -> [Path]
    func v@(String txt) _ | pred txt = [[v]]
    func v l@(_:_) = Prelude.map (v:) (Prelude.concat l)
    func _ _ = []

To obtain a somewhat readable description of one of the paths returned by pathsThatEndInText:

import qualified Data.HashMap.Strict as HM
import qualified Data.Vector as V

describePath :: Path -> [String]
describePath (v:vs) = Prelude.zipWith step (v:vs) vs
  where
    step (Object o) next = (unpack . Prelude.head . HM.keys . HM.filter (==next)) o
    step (Array a) next = (show . maybe (error "not found") id) (V.elemIndex next a)
    step _ _ = error "should not happen"

Finally, here's an example JSON value for testing the above functions in ghci:

exampleJSON :: Value
exampleJSON = maybe Null id (preview _Value str)
  where
    str = "[{ \"k1\" : \"aaa\" },{ \"k2\" : \"ccc\" }, { \"k3\" : \"ddd\" }]"

And here's the gist.

Upvotes: 4

Related Questions