Reputation: 4609
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
Reputation: 27766
The lens
package has useful functions for inspecting tree-like structures like JSON Value
s. 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 Value
s, 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