Christian Gill

Encoding and decoding JSON with Aeson

Originally posted on dev.to/gillchristian.

Encoding and decoding JSON with Aeson

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


almost 4 years ago

Christian Gill