[英]Parse JSON with known variable field
I have a Haskell query
function to get latest token price using我有一个 Haskell query
函数来获取最新的代币价格
https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest
The function takes token id as arg, say 2010
for ADA.该函数将令牌 ID 作为参数,例如 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))
The Json output though has "2010" as a key: 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"}}}}}
Being that 2010
is an arg to query
, I clearly do not want to drill in as data.2010.quote.USD.price
with something like this:由于2010
是query
的 arg,我显然不想像data.2010.quote.USD.price
那样钻取如下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
Question: How can I achieve the flexibility I want?问题:我怎样才能达到我想要的灵活性? Can I somehow pass in the token id to parseJSON
?我可以以某种方式将令牌 ID 传递给parseJSON
吗? Or is there perhaps a Lens-Aeson technique to use a wildcard?或者是否有使用通配符的 Lens-Aeson 技术? ... ...
I you are completely sure that the object inside "data"
will only ever have a single key, we can take the object, convert it into a list of values, fail if the list is empty or has more than one value, and otherwise continue parsing.我完全确定"data"
中的对象永远只有一个键,我们可以获取该对象,将其转换为值列表,如果列表为空或具有多个值则失败,否则继续解析。 Like this:像这样:
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
When there's no key, more than one key, or the value is not an aeson Object
, the pattern [Object _2010O] <-
fails to match and gives an parsing error through the MonadFail
instance of aeson's Parser
.当没有键、多个键或值不是[Object _2010O] <-
Object
,模式[Object _2010O] <-
无法匹配并通过MonadFail
的Parser
的MonadFail
实例给出解析错误。
We could also be a bit more explicit:我们也可以更明确一点:
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可惜我预先知道键名(示例中的“2010”),我在解析时不使用该信息
The problem is that typeclass methods, apart from their own arguments, only have access to static information known at compile time.问题是类型类方法,除了它们自己的参数,只能访问编译时已知的静态信息。 An the tokenId
is likely to be runtime information, for example read from a configuration file. tokenId
可能是运行时信息,例如从配置文件中读取。
Therefore, one solution could involve relying a bit less on the FromJSON
instance.因此,一种解决方案可能涉及减少对FromJSON
实例的依赖。 Instead of parsing Rate
directly, parse to a Value
first (Aeson's Value
has a FromJSON
instance) and then do the Value
to Rate
parsing in a function outside the FromJSON
typeclass, a function that has the tokenId
in scope.不是直接解析Rate
,而是首先解析到一个Value
(Aeson 的Value
有一个FromJSON
实例),然后在FromJSON
类型类之外的函数中进行Value
to Rate
解析,该函数在范围内具有tokenId
。
Still, suppose we want to rely on FromJSON
instances to the greatest degree possible.不过,假设我们希望最大程度地依赖FromJSON
实例。 We could try the "return a function that accepts the data we still don't know" trick, by defining a helper newtype like我们可以尝试“返回一个接受我们仍然不知道的数据的函数”的技巧,通过定义一个像这样的帮助器 newtype
-- we need to pass the tokenId to get the to the Rate
newtype RateWoTokenId = RateWoTokenId (Text -> Result Rate)
And a FromJSON
instance like和一个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
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.