aquavitae
aquavitae

Reputation: 19154

Recursively change a JSON data structure in Haskell

I am trying to write a function that will take a JSON object, make a change to every string value in it and return a new JSON object. So far my code is:

applyContext :: FromJSON a => a -> a
applyContext x =
  case x of
    Array _  -> map applyContext x
    Object _ -> map applyContext x
    String _ -> parseValue x
    _        -> x

However, the compiler complains about second second case line:

Couldn't match expected type `[b0]' with actual type `a'
  `a' is a rigid type variable bound by
    the type signature for:
      applyContext :: forall a. FromJSON a => a -> a
    at app\Main.hs:43:17

I'm guessing that is because map is meant to work on lists, but I would have naively expected it to use Data.HashMap.Lazy.map instead, since that is what the type actually is in that case. If I explicitly use that function I get

Couldn't match expected type `HashMap.HashMap k0 v20' with actual type `a'

which also makes sense, since I haven't constrained a to that extent because then it wouldn't work for the other cases. I suspect that if I throw enough explicit types at this I could make it work but it feels like it should be a lot simpler. What is an idiomatic way of writing this function, or if this is good then what would be the simplest way of getting the types right?

Upvotes: 2

Views: 223

Answers (1)

freestyle
freestyle

Reputation: 3790

First of all, what FromJSON a => a does mean? It's type of some thing what says: it can be thing with any type but only from class FromJSON. This class can contain types which very differently constructed and you can't do any pattern matching. You can only do what is specified in the class FromJSON declaration by programmer. Basically, there is one method parseJSON :: FromJSON a => Value -> Parser a.

Secondly, you should use some isomorphic representation of JSON for your work. The type Value is good one. So, you can do the main work by the function like Value -> Value. After that, you can compose this fuction with parseJSON and toJSON for generalse types.

Like this:

change :: Value -> Value
change (Array x)  = Array . fmap change $ x
change (Object x) = Object . fmap change $ x
change (String x) = Object . parseValue $ x
change x          = x

apply :: (ToJSON a, FromJSON b) => (Value -> Value) -> a -> Result b
apply change = fromJSON . change . toJSON

unsafeApply :: (ToJSON a, FromJSON b) => (Value -> Value) -> a -> b
unsafeApply change x = case apply change x of
                         Success x -> x
                         Error msg -> error $ "unsafeApply: " ++ msg

applyContext :: (ToJSON a, FromJSON b) => a -> b
applyContext = unsafeApply change

You can write more complicated transformations like Value -> Value with lens and lens-aeson. For example:

import Control.Lens
import Control.Monad.State
import Data.Aeson
import Data.Aeson.Lens
import Data.Text.Lens
import Data.Char

change :: Value -> Value
change = execState go
  where
    go = do
        zoom values go
        zoom members go
        _String . _Text . each %= toUpper
        _Bool %= not
        _Number *= 10

main = print $ json & _Value %~ change
  where json = "{\"a\":[1,\"foo\",false],\"b\":\"bar\",\"c\":{\"d\":5}}"

Output will be:

"{\"a\":[10,\"FOO\",true],\"b\":\"BAR\",\"c\":{\"d\":50}}"

Upvotes: 1

Related Questions