Изолировать одно значение из вложенного ответа 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\"}}"
Другие вопросы по тегам