In the past days I've been working on a small app to parse the transactions statements I get from my bank with the idea to generate something that is easier to visualize.
I get a .txt
file where each line corresponds to one transaction and from that I wanted to generate a JSON file that then I can use in a charting app.
In the process I learned a few things about working with JSON in Haskell using Aeson.
To decode and encode values from and to JSON we use the decode
and encode
methods (good naming there 😉).
decode :: FromJSON a => ByteString -> Maybe a
encode :: ToJSON a => a -> ByteString
NOTE: By setting the OverloadedStrings compiler extension we can treat string literals as not only String
but also ByteString
, Text
, and others.
We can learn a lot about how Aeson works from these type signatures.
decode
takes a ByteString
(we can think of it as a string) and returns a Maybe a
with the constraint that this a
has to have the FromJSON
type class. With that name we can assume that it's for types that can be parsed from JSON.
encode
on the other hand takes a value of type a
, this time with the ToJSON
type class and returns a string. In this case we can assume ToJSON
means a type that can be converted to JSON.
Let's then try to use them. Notice that we need to specify what type we are decoding/encoding so the compiler know what instance of FromJSON/TOJSON should use.
λ> :set -XOverloadedStrings
λ> import Data.Aeson
λ> decode "true" :: Maybe Bool
Just True
λ> decode "tru" :: Maybe Bool
Nothing
λ> decode "123" :: Maybe Bool
Nothing
λ> decode "123" :: Maybe Int
Just 123
λ> encode (123 :: Int)
"123"
λ> encode ("hola" :: String)
"\"hola\""
λ> encode (Just 123 :: Maybe Int)
"123"
λ> encode (Nothing :: Maybe Int)
"null"
Decoding returns a Maybe a
because the operation can fail since we don't know if the JSON in the string is properly formatted. Whereas encoding, since we have the ToJSON
constraint should always succeed.
All of those types "work out of the box" because the instances are already implemented by Aeson 🎉
λ> :i Int
data Int = ghc-prim-0.5.3:GHC.Types.I# ghc-prim-0.5.3:GHC.Prim.Int#
-- Defined in ‘ghc-prim-0.5.3:GHC.Types’
...
instance ToJSON Int
-- Defined in ‘aeson-1.4.2.0:Data.Aeson.Types.ToJSON’
instance FromJSON Int
-- Defined in ‘aeson-1.4.2.0:Data.Aeson.Types.FromJSON’
But we are not going to work only with primitive types, are we?
But if we try to encode a value of a custom type we get an error.
λ> data Obj = Obj { id :: Int }
λ> encode Obj { id = 1 }
<interactive>:5:1: error:
• No instance for (ToJSON Obj) arising from a use of ‘encode’
• In the expression: encode Obj {id = 1}
In an equation for ‘it’: it = encode Obj {id = 1}
We need to implement ToJSON
for this type.
λ> :i ToJSON
class ToJSON a where
toJSON :: a -> Value
default toJSON :: (GHC.Generics.Generic a,
aeson-1.4.2.0:Data.Aeson.Types.ToJSON.GToJSON
Value Zero (GHC.Generics.Rep a)) =>
a -> Value
toEncoding :: a -> Encoding
toJSONList :: [a] -> Value
toEncodingList :: [a] -> Encoding
-- Defined in ‘aeson-1.4.2.0:Data.Aeson.Types.ToJSON’
We could do it manually:
{-# LANGUAGE OverloadedStrings #-}
import Data.Aeson
newtype Obj = Obj
{ a :: Int
}
instance ToJSON Obj where
toJSON Obj {a = a} = object [("a", toJSON a)]
-- object :: [Pair] -> Value
-- type Pair = (Text, Value) -- Value here is the Aeson.Value
Or we can use some compiler magic to do it for us using the compiler extensions: DeriveAnyClass & DeriveGeneric.
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
import Data.Aeson
newtype Obj = Obj
{ a :: Int
} deriving (Generic, ToJSON)
Et voilà 🎉
That is what we'll be doing most of the time, unless we to encode or decode our data in some particular way.
Implementing and deriving FromJSON
works pretty much the same so it's not worth to show it.
Conclusion
The compiler magic of deriving type classes instances makes it really easy to work with JSON in Haskell. We still have the safety of our types without having to implement custom encoder/decoders for everything like other languages (e.g. Elm).
Top comments (0)