Home > other >  Parse JSON with known variable field
Parse JSON with known variable field

Time:10-25

I have a Haskell query function to get latest token price using

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

The function takes token id as arg, say 2010 for ADA.

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:

{"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:

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? Or is there perhaps a Lens-Aeson technique to use a wildcard? ...

CodePudding user response:

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

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"

CodePudding user response:

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

The problem is that typeclass methods, apart from their own arguments, only have access to static information known at compile time. And the tokenId is likely to be runtime information, for example read from a configuration file.

Therefore, one solution could involve relying a bit less on the FromJSON instance. 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.


Still, suppose we want to rely on FromJSON instances to the greatest degree possible. We could try the "return a function that accepts the data we still don't know" trick, by defining a helper newtype like

-- we need to pass the tokenId to get the to the Rate
newtype RateWoTokenId = RateWoTokenId (Text -> Result Rate) 

And a FromJSON instance like

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          
  • Related