Isolate a single value from a nested JSON response

2019-06-02 09:58发布

问题:

I'm working with a couple of JSON based APIs and the vast majority of the time I only need to extract a single value from the JSON response. E.g. with {"foo":"xyz","bar":"0.0000012"} I only need the value of bar.

To accommodate this I've written functions to extract the information I need:

-- | Isolate a Double based on a key from a JSON encoded ByteString
isolateDouble :: String -> B.ByteString -> Maybe Double
isolateDouble k bstr = isolateString k bstr >>= maybeRead

-- | Isolate a String based on a key from a JSON encoded ByteString
isolateString :: String -> B.ByteString -> Maybe String
isolateString k bstr = decode bstr >>= parseMaybe (\obj -> obj .: pack k :: Parser String)

Unfortunately, one of the APIs sends the response like this: {"success":"true","message":"","result":{"foo":"xyz","bar":"0.0000012"}}

Obviously passing that to isolateDouble "bar" results in Nothing

I think the last time I did this I wrote fromJSON instances for both levels of the response like so:

data Response = Response !Text !Text !Result

instance FromJSON Response where
   parseJSON (Object v) = Response <$> v .: "success" <*> v .: "message" <*> v .: "result"
   parseJSON _ = mzero

data Result = Result !Text !Text

instance FromJSON Result where
   parseJSON (Object v) = Result <$> v .: "foo" <*> v .: "bar"
   parseJSON _ = mzero

But then I would have to repeat that for dozens of different API calls. I know I can also use derive generics, but unfortunately some of the indexes in the JSON responses have clashing names like "id".

Given all that, what would be the best way to isolate a single value from a nested JSON response?

回答1:

You can do this with or without lens. Lenses are nice because they allow you to compose the lenses you write with other lenses later. However if you don't need this it might be overkill.

First realize that when you write a FromJSON instance you write a function with this signature parseJSON :: Value -> Parser a. You can easily write these functions without using the FromJSON typeclass. What you actually want to do is write 2 parsers like this, then compose them.

First you need to write the one that will look up a 'bar' in a object and parse it to a Double:

parseBar :: Value -> Parser Double
parseBar (Object o) = o .: "bar" >>= maybe (fail "Not a double") return . maybeRead . unpack
parseBar _          = fail "Expected an object."

Now you can write another function that uses this function to parse a more nested value:

parseNested :: Value -> Parser Double
parseNested (Object o) = o .: "result" >>= parseBar
parseNested _          = fail "Expected an object."

Now we write a utility function that runs the parser on a ByteString:

runParser :: (Value -> Parser a) -> BL.ByteString -> Maybe a 
runParser p bs = decode bs >>= parseMaybe p

Now we can use this function with the parsers we defined above to parse the json values like this:

testParseBar = runParser parseBar "{\"foo\":\"xyz\",\"bar\":\"0.0000012\"}"
testParseNested = runParser parseNested "{\"success\":\"true\",\"message\":\"\",\"result\":{\"foo\":\"xyz\",\"bar\":\"0.0000012\"}}"

Note that you can also use the Alternative instance on Parsers to create one parser that will parse either of these values:

parseBarOrNested :: Value -> Parser Double
parseBarOrNested v = parseBar v <|> parseNested v

This parser will try the bar parser first, if it doesnt work it will use the nested parser.

testBarOrNestedBar = runParser parseBarOrNested "{\"foo\":\"xyz\",\"bar\":\"0.0000012\"}"
testBarOrNestednested = runParser parseBarOrNested "{\"success\":\"true\",\"message\":\"\",\"result\":{\"foo\":\"xyz\",\"bar\":\"0.0000012\"}}"

Here is the complete code with pragmas and imports:

{-# LANGUAGE OverloadedStrings #-}

import Data.Aeson
import Data.Aeson.Types
import Control.Applicative

import qualified Data.ByteString.Lazy as BL
import Data.Text (unpack)

-- Replace with your implementation
maybeRead = Just . read


parseBar :: Value -> Parser Double
parseBar (Object o) = o .: "bar" >>= maybe (fail "Not a double") return . maybeRead . unpack
parseBar _          = fail "Expected an object."

parseNested :: Value -> Parser Double
parseNested (Object o) = o .: "result" >>= parseBar
parseNested _          = fail "Expected an object."

parseBarOrNested :: Value -> Parser Double
parseBarOrNested v = parseBar v <|> parseNested v

runParser :: (Value -> Parser a) -> BL.ByteString -> Maybe a 
runParser p bs = decode bs >>= parseMaybe p


testParseBar = runParser parseBar "{\"foo\":\"xyz\",\"bar\":\"0.0000012\"}"

testParseNested = runParser parseNested "{\"success\":\"true\",\"message\":\"\",\"result\":{\"foo\":\"xyz\",\"bar\":\"0.0000012\"}}"

testBarOrNestedBar = runParser parseBarOrNested "{\"foo\":\"xyz\",\"bar\":\"0.0000012\"}"

testBarOrNestednested = runParser parseBarOrNested "{\"success\":\"true\",\"message\":\"\",\"result\":{\"foo\":\"xyz\",\"bar\":\"0.0000012\"}}"


标签: haskell aeson