用已知变量字段解析 JSON

Parse JSON with known variable field

我有一个 Haskell query 函数可以使用

获取最新的代币价格

https://coinmarketcap.com/api/documentation/v1/#operation/getV1CryptocurrencyQuotesLatest

该函数将代币 id 作为 arg,对于 ADA 来说 2010

import Data.Aeson
import Network.HTTP.Req

newtype Rate = Rate Double

query :: Int -> IO (Either Text Rate)
query tokenId = 
    let
        url = https queryPrefix /: "v1" /: "cryptocurrency" /: "quotes" /: "latest"
        idParam = "id" =: tokenId
        options = standardHeader <> idParam
    in
        runReq defaultHttpConfig $ do
            v <- req GET url NoReqBody jsonResponse options
            let responseCode = responseStatusCode v

            if isValidHttpResponse responseCode then do  
                case fromJSON $ responseBody v of
                    Success x -> pure $ Right x
                    Error e -> pure $ Left $ pack $ "Error decoding state: " <> e
            else
                pure $ Left $ pack ("Error with CoinMarketCap query 'Quotes Latest': " <> show responseCode <> ".  " <> show (responseStatusMessage v))              

Json 输出虽然以“2010”为键:

{"status":
    {"timestamp":"2021-10-24T03:35:01.583Z","error_code":0,"error_message":null,"elapsed":163,"credit_count":1,"notice":null}
,"data":
    {"2010":
        {"id":2010
        ,"name":"Cardano"
        ,"symbol":"ADA"
        ,"slug":"cardano"
        ,"num_market_pairs":302,"date_added":"2017-10-01T00:00:00.000Z"
        ,"tags":["mineable","dpos","pos","platform","research","smart-contracts","staking","binance-smart-chain","cardano-ecosystem"]
        ,"max_supply":45000000000
        ,"circulating_supply":32904527668.666
        ,"total_supply":33250650235.236,"is_active":1
        ,"platform":null
        ,"cmc_rank":4
        ,"is_fiat":0
        ,"last_updated":"2021-10-24T03:33:31.000Z"
        ,"quote":
            {"USD":
                {"price":2.16109553945978
                ,"volume_24h":2048006882.386299
                ,"volume_change_24h":-24.06,"percent_change_1h":0.24896227
                ,"percent_change_24h":0.38920394
                ,"percent_change_7d":-0.97094597
                ,"percent_change_30d":-6.13245906
                ,"percent_change_60d":-21.94246757
                ,"percent_change_90d":63.56901345
                ,"market_cap":71109827972.785
                ,"market_cap_dominance":2.7813
                ,"fully_diluted_market_cap":97249299275.69,"last_updated":"2021-10-24T03:33:31.000Z"}}}}}

由于 2010query 的参数,我显然不想像 data.2010.quote.USD.price 那样深入研究:

instance FromJSON Rate where
    parseJSON = withObject "Rate" $ \o -> do
        dataO  <- o .: "data"
        _2010O <- dataO .: "2010" -- #############
        quoteO <- _2010O .: "quote"
        usdO <- quoteO .: "USD"
        price <- usdO .: "price"
        
        pure $ Rate price  

问:我怎样才能达到我想要的灵活性?我能以某种方式将令牌 ID 传递给 parseJSON 吗?或者是否有使用通配符的 Lens-Aeson 技术? ...

你完全确定 "data" 中的对象只会有一个键,我们可以获取对象,将其转换为值列表,如果列表为空或有更多值则失败大于一个值,否则继续解析。像这样:

instance FromJSON Rate where
    parseJSON = withObject "Rate" $ \o -> do
        Object dataO  <- o .: "data" -- we expect an Object
         -- get the single value, it should be an Object itself
        [Object _2010O] <- pure $ Data.Foldable.toList dataO
        quoteO <- _2010O .: "quote"
        usdO <- quoteO .: "USD"
        price <- usdO .: "price"
        pure $ Rate price 

当没有键、多个键或值不是 aeson Object 时,模式 [Object _2010O] <- 无法匹配并通过 MonadFail 给出解析错误aeson 的实例 Parser.

我们还可以更明确一点:

instance FromJSON Rate where
    parseJSON = withObject "Rate" $ \o -> do
        Object dataO  <- o .: "data"
        let objects = Data.Foldable.toList dataO
        case objects of
            [Object _2010O] -> do
                quoteO <- _2010O .: "quote"
                usdO <- quoteO .: "USD"
                price <- usdO .: "price"
                pure $ Rate price  
            [_] -> fail "value is not Object"
            _ -> fail "zero or more than one key"

it seems a pity that being that I know the key name upfront ("2010" in the example), I do not use that info when parsing

问题在于,类型类方法除了它们自己的参数外,只能访问编译时已知的 static 信息。并且 tokenId 很可能是运行时信息,例如从配置文件中读取。

因此,一种解决方案可能涉及减少对 FromJSON 实例的依赖。与其直接解析 Rate,不如先解析为 Value(Aeson 的 Value 有一个 FromJSON 实例),然后将 Value 解析为 Rate parsing 在函数 外部 FromJSON 类型类中,一个在范围内具有 tokenId 的函数。


不过,假设我们想最大程度地依赖 FromJSON 个实例。我们可以尝试“return 一个接受我们仍然不知道的数据的函数”的技巧,通过定义一个像

这样的辅助新类型
-- we need to pass the tokenId to get the to the Rate
newtype RateWoTokenId = RateWoTokenId (Text -> Result Rate) 

还有一个 FromJSON 实例,例如

instance FromJSON RateWoTokenId where
    parseJSON = withObject "Rate" $ \o -> do
        dataO  <- o .: "data"
        pure $ RateWoTokenId $ \tokenId -> -- returning a function here!
            -- We continue parsing inside the function,
            -- because the tokenId is known there.
            flip Data.Aeson.Types.parse dataO $ \dataO -> do                   
                _2010O <- dataO .: Data.Aeson.Key.fromText tokenId
                quoteO <- _2010O .: "quote"
                usdO <- quoteO .: "USD"
                price <- usdO .: "price"
                pure $ Rate price