user3594595
user3594595

Reputation:

Extract nested property inside Aeson object

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

Answers (2)

Łukasz
Łukasz

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

danidiaz
danidiaz

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

Related Questions