Reputation:
How can I get a nested property using Data.Aeson
?
For example, when decoding an arbitrary JSON string using Value
like this:
decode "{\"foo\":{\"bar0\":\"foobar0\",
\"bar1\":\"foobar1\"}}" :: Maybe Value
I end up with this:
Just (Object (fromList [("foo",Object (fromList [("bar1",String "foobar1"),("bar0",String "foobar0")]))]))
Now, how can I write a function [String] -> Object -> Maybe Value
that will extract the Value
, if any, arrived at by following the provided list of properties?
This function should be used like so:
extractProperty ["foo", "bar0"] (Object (fromList [("foo",Object (fromList [("bar1",String "foobar1"),("bar0",String "foobar0")]))]))
==> Just (String "foobar0")
extractProperty ["foo", "bar0", "baz"] (Object (fromList [("foo",Object (fromList [("bar1",String "foobar1"),("bar0",String "foobar0")]))]))
==> Nothing
Upvotes: 2
Views: 732
Reputation: 36191
Another approach, using monadic bind:
import Data.Text (Text)
import qualified Data.HashMap.Strict as HM
extractProperty :: [Text] -> Value -> Maybe Value
extractProperty [] v = Just v
extractProperty (k:ks) (Object o) = HM.lookup k o >>= prop ks
extractProperty _ _ = Nothing
Upvotes: 0
Reputation: 27771
The following solution makes use of the lens
and lens-aeson
packages:
{-# LANGUAGE FlexibleInstances #-}
import Control.Lens (view,pre,ix) -- from lens
import Control.Lens.Reified (ReifiedTraversal(..))
import Data.Aeson -- from aeson
import Data.Aeson.Lens (_Object) -- from lens-aeson
import Data.Text -- form text
instance Monoid (ReifiedTraversal s s s s) where
mempty = Traversal id
mappend (Traversal t1) (Traversal t2) = Traversal (t1 . t2)
extractProperty :: [Text] -> Object -> Maybe Value
extractProperty keys o =
view (pre telescope) (Object o)
where
telescope = runTraversal $ foldMap (\k -> Traversal $ _Object . ix k) keys
ReifiedTraversal
is simply a newtype around a Traversal
, we define a Monoid
instance on it to allow easy composition of traversals that start and end in the same type (similar to how the Endo
monoid works).
In our case, the traversal _Object . ix k
goes from Value
to Value
. ix
comes from Control.Lens.At
and indexes on the map of properties of an Object
.
We extract the first result of the composed traversal (if it exists) with the pre
function.
Edit: As @cchalmers mentions in his comment, there's no need to declare an orphan instance, it works fine just with Endo
. Also key k
is the same as _Object . ix k
.
Here's a version of extractProperty
that doesn't use lens
and instead relies on composing a list of kleisli arrows Value -> Maybe Value
using foldr
:
import qualified Data.HashMap.Strict as HM
extractProperty :: [T.Text] -> Object -> Maybe Value
extractProperty keys o = telescope keys (Object o)
where
telescope = foldr (>=>) return . map maybeKey
maybeKey k v = case v of
Object o -> HM.lookup k o
_ -> Nothing
Perhaps lens
was a bit overkill in this case.
Upvotes: 0