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?
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 signatureparseJSON :: 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
:Now you can write another function that uses this function to parse a more nested value:
Now we write a utility function that runs the parser on a
ByteString
:Now we can use this function with the parsers we defined above to parse the json values like this:
Note that you can also use the
Alternative
instance on Parsers to create one parser that will parse either of these values:This parser will try the bar parser first, if it doesnt work it will use the nested parser.
Here is the complete code with pragmas and imports: