I'm relatively new to haskell and right now I'm trying to get a deeper understanding and trying to get used to different popular libraries.
Right now I'm trying "aeson".
What I want to do is parse MSFT quote request from https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=MSFT&apikey=demo
This is what it looks like
{
"Global Quote": {
"01. symbol": "MSFT",
"02. open": "105.3500",
"03. high": "108.2400",
"04. low": "105.2700",
"05. price": "107.6000",
"06. volume": "23308066",
"07. latest trading day": "2018-10-11",
"08. previous close": "106.1600",
"09. change": "1.4400",
"10. change percent": "1.3564%"
}
}
This is what I've got so far
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
import Data.Aeson
import qualified Data.ByteString.Lazy as B
import GHC.Exts
import GHC.Generics
import Network.HTTP
import Network.URI
jsonURL :: String
jsonURL = "http://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=MSFT&apikey=demo"
getRequest_ :: HStream ty => String -> Request ty
getRequest_ s = let Just u = parseURI s in defaultGETRequest_ u
jsonReq = getRequest_ jsonURL
data Quote = Quote {quote :: String,
symbol :: String,
open :: Float,
high :: Float,
low :: Float,
price :: Float,
volume :: Float,
ltd :: String,
previousClose :: Float,
change :: Float,
changePerct :: Float
} deriving (Show, Generic)
instance FromJSON Quote
instance ToJSON Quote
main :: IO ()
main = do
d <- simpleHTTP jsonReq
body <- getResponseBody d
print (decode body :: Maybe Quote)
What am I doing wrong?
Edit: Fixed version in the answers.
First off: Aeson is not the easiest library for a beginner. There are more difficult ones, sure, but it supposes you already a fair number of things about the language. You didn't pick the "simplest task" to begin with. I know this can be surprising, and you might think that parsing JSON should be simple, but parsing JSON with strong type guarantees is actually not that simple.
But here's what I can tell you to help you a bit:
First, use eitherDecode
rather than decode
: you will get an error message rather than simply Nothing
, which will help you a bit.
Deriving through Generic
is neat and very often, a time saver, but it's not magic either. The name of the object key and the name of your datatype fields have to match exactly. Sadly, this is not the case here and due to haskell syntax, you couldn't name your fields like the keys of the object. Your best solution is to implement FromJSON manually (see the recommended link below). A good way to see "what is expect" by the generic FromJSON is to also derive ToJSON, create a dummy Quote
and see the result of encode
.
Your first field (quote
) is not a key of the object itself, but rather the name of this object. So you have dynamic keys ("Global Quote" being one here). Once again, this typically a case where you want to write the FromJSON instance manually.
I recommend you read this famous tutorial written by Artyom Kazak on Aeson. This will help you tremendously and is probably the best advice I can give.
For your manual instance, supposing it was exactly the document you want to parse and you had only the "Global Quote" to deal with, it would look more or less like this:
instance ToJSON Quote where
parseJSON = withObject "Document" $
\d -> do
glob <- d .: "Global Quote"
withObject "Quote" v (\gq ->
Quote <$> gq .: "01. symbol"
<*> pure "Global Quote"
<*> gq .: "02. open"
<*> gq .: "03. high"
-- ... and so on
) v
(It's not the most pretty way, nor the best way, to write it, but it should be one possible way).
Also note that, as an astute commenter wrote, the types of your fields are not always aligned with the type of your example JSON document. "volume" is an Int
(byte-limited int), potentially an Integer
("mathematical" integer, no bound), but not a Float
. Your "ltd" can be parsed a string - but it probably should be a date (Day
from Data.Time
would be the first choice - it already has a FromJSON
instance so chances are it should be parseable as is). Change percent is most likely not parseable as Float like that, you'll need to write a dedicated parser for this type (and decide how you want to store it - Ratio
is a potential solution).
@Raveline with their answer above pointed me to the right direction. I was able to solve all those issues, here's the final product!
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Test where
import Data.Aeson
import qualified Data.ByteString.Lazy as B
import GHC.Exts
import GHC.Generics
import Network.HTTP.Conduit (simpleHttp)
jsonURL :: String
jsonURL = "https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=MSFT&apikey=demo"
getJSON :: IO B.ByteString
getJSON = simpleHttp jsonURL
data Quote = Quote {
symbol :: String,
open :: String,
high :: String,
low :: String,
price :: String,
volume :: String,
ltd :: String,
previousClose :: String,
change :: String,
changePercent :: String
} deriving (Show, Generic)
instance FromJSON Quote where
parseJSON = withObject "Global Quote" $
\o -> do
globalQuote <- o .: "Global Quote"
symbol <- globalQuote .: "01. symbol"
open <- globalQuote .: "02. open"
high <- globalQuote .: "03. high"
low <- globalQuote .: "04. low"
price <- globalQuote .: "05. price"
volume <- globalQuote .: "06. volume"
ltd <- globalQuote .: "07. latest trading day"
previousClose <- globalQuote .: "08. previous close"
change <- globalQuote .: "09. change"
changePercent <- globalQuote .: "10. change percent"
return Quote {..}
main :: IO ()
main = do
d <- (eitherDecode <$> getJSON) :: IO (Either String Quote)
case d of
Left e -> print e
Right qt -> print (read (price qt) :: Float)