Haskell: Reusing FromJSON instances with lenses, l

2020-05-23 20:04发布

问题:

I have been playing with Aeson and the lens package (lens-aeson, migrated from the core lens package), and have been sruggling to get them to work together.

As a toy example, I have a type:

data Colour = Yellow | Green | Blue

and the FromJSON instance:

instance FromJSON Colour where
    parseJSON (String s) = return $ case s of
        "blue" -> Blue
        "green" -> Green
        _ -> Yellow
    parseJSON _ = mzero

So far so good.

Now, say I have some nested JSON data come in that I want to extract just this out of:

{
    "info": {
        "colour": "yellow"
    },
    /* other props */
}

I don't care about the rest, only this "colour" value. To make matters worse, lets say that the JSON isn't particularly consistent, so sometimes I have

{ "item": { "colour": "yellow" } }

and other times

{ "random": {"item_colour": "yellow"} }

I want to be able to get at the colour value as easily as possible, and then parse it ideally using my FromJSON instance ideally into a colour. This is a toy example, but instead of Colour the data type might have a number of fields etc.

I started looking at the lens-aeson stuff and that got my hopes up; it allows very easy peering into a JSON structure. example:

> "{ \"info\": { \"colour\": \"yellow\" } }" ^? key "info" . key "colour"
Just (String "yellow")
> "{ \"info\": { \"colour\": \"yellow\" } }" ^? key "info" . key "colour" . _String
Just "yellow"

But I can't find a way to run that through my parseJSON call to get back Just Yellow. parseJSON seems close in that it takes the right type (thing inside the maybe as least) but then falls apart after that. Ideally I'd be able to do something like one of:

> "{ \"info\": { \"colour\": \"yellow\" } }" ^? key "info" . key "colour" . _ParseJSON :: Maybe Colour
Just Yellow
> "{ \"info\": { \"colour\": \"yellow\" } }" ^? key "info" . key "colour" . _Colour
Just Yellow

The closest I've come to figuring it out is re-encoding then decoding the result of the above, eg:

> encode $ "{ \"info\": { \"colour\": \"yellow\" } }" ^? key "info" . key "colour"
"\"yellow\""

which gives me back the bit of JSON encoded data I want. In a more complex case, if that data was an object or array, I could then run decode on it as I would normally to get back my more complex type, but decode doesn't like improper JSON; things that arent wrapped in array or object syntax. In addition, doing decode then encode seems horribly messy and not very performant.

I am rather new to lenses and Aeson as a whole (and Haskell for that matter, though I've come to understand things like monads/applicatives on the whole so slowly making progress!). How would you guys go about getting this done?

My general motivation is that I will be working with a load of JSON data, but actually only care about fragments of it, and would thus rather avoid declaring a data type each time I need to get those fragments out from different places in the JSON, and instead just declare types for the bits I care about.

Note that I am using lens-aeson-1 and lens-4.4.0.1, rather than aeson-lens which works a little differently (it might be relevant in answers)!

Thanks in advance! James

回答1:

You can use the _JSON prism. You only need to write a ToJSON instance for Colour:

instance ToJSON Colour where
  toJSON Yellow = String "yellow"
  toJSON Blue   = String "blue"
  toJSON Green  = String "green"

and then you can use it like this:

parseColour :: String -> Maybe Colour
parseColour j = j ^? key "info" . key "colour" . _JSON
-- point-free: parseColor = preview $ key "info" . key "colour" . _JSON

-- In GHCi
λ: parseColour "{ \"info\": { \"colour\": \"yellow\" } }"
Just Yellow