Изолировать одно значение из вложенного ответа JSON в Aeson
Я работаю с несколькими API на основе JSON, и в большинстве случаев мне нужно извлечь только одно значение из ответа 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?
1 ответ
Вы можете сделать это с или без объектива. Линзы хороши тем, что позволяют составлять линзы, которые вы пишете, с другими линзами позже. Однако, если вам это не нужно, это может быть излишним.
Сначала поймите, что когда вы пишете FromJSON
Например, вы пишете функцию с этой подписью parseJSON :: Value -> Parser a
, Вы можете легко написать эти функции без использования класса типов FromJSON. Что вы на самом деле хотите сделать, так это написать 2 парсера, а затем составить их.
Сначала вам нужно написать тот, который будет искать "бар" в объекте и анализировать его 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\"}}"
Обратите внимание, что вы также можете использовать Alternative
Например, в Parsers для создания одного анализатора, который будет анализировать одно из следующих значений:
parseBarOrNested :: Value -> Parser Double
parseBarOrNested v = parseBar v <|> parseNested v
Этот синтаксический анализатор сначала попробует анализатор панели, если он не работает, он будет использовать вложенный анализатор.
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\"}}"