从 Aeson 中的嵌套 JSON 响应中隔离单个值

Isolate a single value from a nested JSON response in Aeson

我正在处理几个基于 JSON 的 APIs 并且绝大多数时候我只需要从 JSON 响应中提取一个值。例如。使用 {"foo":"xyz","bar":"0.0000012"} 我只需要 bar.

的值

为此,我编写了函数来提取我需要的信息:

-- | 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)

不幸的是,其中一个 API 发送了这样的响应: {"success":"true","message":"","result":{"foo":"xyz","bar":"0.0000012"}}

显然将其传递给 isolateDouble "bar" 会导致 Nothing

我想我上次这样做时我为响应的两个级别写了 fromJSON 个实例,如下所示:

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

但随后我将不得不针对数十个不同的 API 调用重复该操作。我知道我也可以使用派生泛型,但不幸的是 JSON 响应中的一些索引有冲突的名称,例如 "id".

考虑到所有这些,将单个值与嵌套 JSON 响应隔离开来的最佳方法是什么?

有无镜头均可。镜头很好,因为它们允许您稍后将您编写的镜头与其他镜头组合在一起。但是,如果你不需要这个,它可能会有点矫枉过正。

首先要意识到,当您编写一个 FromJSON 实例时,您会编写一个具有此签名 parseJSON :: Value -> Parser a 的函数。您无需使用 FromJSON 类型类即可轻松编写这些函数。你真正想要做的是像这样编写 2 个解析器,然后组合它们。

首先,您需要编写一个在对象中查找 'bar' 并将其解析为 Double:

的程序
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."

现在我们编写一个实用函数,在 ByteString:

上运行解析器
runParser :: (Value -> Parser a) -> BL.ByteString -> Maybe a 
runParser p bs = decode bs >>= parseMaybe p

现在我们可以将此函数与我们上面定义的解析器一起使用来解析 json 值,如下所示:

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

请注意,您还可以使用 Parsers 上的 Alternative 实例来创建一个解析器来解析以下任一值:

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

此解析器将首先尝试 bar 解析器,如果它不起作用,它将使用嵌套解析器。

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

这里是包含编译指示和导入的完整代码:

{-# 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\"}}"