Search code examples
haskellhaskell-lensaeson

Cleanly accessing nested data in lens-aeson


I have the following bit of code, and I'm not happy with it:


import Data.Aeson.Lens
import Data.Aeson.Types
import Data.Text hiding (foldl, map)

path :: (Applicative f, AsValue t) => [Text] -> (Value -> f Value) -> t -> f t path [] = error "No path provided" path (i:is) = foldl (.) (key i) (map key is)

path is meant to be a lens into a nested JSON object. For instance:

"{ \"a\": { \"b\": 8} }" ^? path ["a", "b"] == Just (Number 8.0)

Currently this works, but the somewhat obvious shortcoming is that the function is not total.

I tried the following definitions:

  1. path is = foldl (.) id (map key is)
    
  2. path [] = id
    path (i:is) = foldl (.) (key i) (map key is)
    
  3. -- This one doesn't compile
    path [] = _Object
    path (i:is) = foldl (.) (key i) (map key is)
    

Running the same code block as above with these definitions yields Nothing, instead of the expected 8.

I'm almost sure this is a case of type juggling that I'm just missing, but I can't figure out how to make this work with a total function. The expected behavior is for path [] to be a Lens which focused on the whole json object.


Solution

  • Use _Value for the empty case:

    > ("null" :: String) ^? _Value
    Just Null
    > ("null" :: String) ^? (foldl (.) _Value $ map key ["foo", "bar"])
    Nothing
    > ("{ \"a\": { \"b\": 8} }" :: String) ^? (foldl (.) _Value $ map key ["a", "b"])
    Just (Number 8.0)
    

    Also path as you describe cannot be a Lens as it's partial (as you mention!), at best it is Traversal. Yet if you are really sure what you are doing, than you can use signular to convert Traversal into Lens.