JSON with aeson
Prerequisites: Type classes
aeson is Haskell's standard JSON library. You describe how your
types map to JSON with the ToJSON and FromJSON classes — usually by
deriving them — and aeson handles encoding, decoding, and validation.
Deriving instances with Generic
{-# LANGUAGE DeriveGeneric, DeriveAnyClass, OverloadedStrings #-}
import Data.Aeson
import GHC.Generics (Generic)
import qualified Data.ByteString.Lazy.Char8 as BL
data User = User
{ name :: String
, age :: Int
, email :: Maybe String
}
deriving (Show, Generic, ToJSON, FromJSON)
main :: IO ()
main = do
BL.putStrLn (encode (User "Ada" 36 Nothing))
BL.putStrLn (encode [User "Alan" 41 (Just "alan@example.com")])
print (decode "{\"name\":\"Grace\",\"age\":85}" :: Maybe User)
print (eitherDecode "{\"name\":\"Grace\"}" :: Either String User)
print (eitherDecode "{\"name\":\"Grace\",\"age\":\"old\"}" :: Either String User)
{"age":36,"email":null,"name":"Ada"}
[{"age":41,"email":"alan@example.com","name":"Alan"}]
Just (User {name = "Grace", age = 85, email = Nothing})
Left "Error in $: parsing Main.User(User) failed, key \"age\" not found"
Left "Error in $.age: parsing Int failed, expected Number, but encountered String"
encodeproduces a lazyByteString;decodereturns aMaybe, andeitherDecodean error message saying what was wrong and where.- A
Maybefield becomesnullwhen encoding, and may be missing when decoding. - The keys come out in alphabetical order, because these derived
instances build an aeson
Value(whose objects are sorted maps) before encoding it. JSON object order carries no meaning, so this rarely matters. OverloadedStringslets the JSON string literals beByteStrings.- Decoding is validation: if the JSON doesn't match the type, you get an error instead of a half-filled object.
Custom field names
JSON often uses a different naming style. genericToJSON and
genericParseJSON take options:
{-# LANGUAGE DeriveGeneric, OverloadedStrings #-}
import Data.Aeson
import GHC.Generics (Generic)
import qualified Data.ByteString.Lazy.Char8 as BL
data Order = Order {orderId :: Int, orderTotalCents :: Int}
deriving (Show, Generic)
options :: Options
options = defaultOptions {fieldLabelModifier = camelTo2 '_' . drop 5}
instance ToJSON Order where
toJSON = genericToJSON options
instance FromJSON Order where
parseJSON = genericParseJSON options
main :: IO ()
main = do
BL.putStrLn (encode (Order 7 1250))
print (decode "{\"id\":8,\"total_cents\":999}" :: Maybe Order)
{"id":7,"total_cents":1250}
Just (Order {orderId = 8, orderTotalCents = 999})
Hand-written instances
For full control, write the instances yourself:
{-# LANGUAGE OverloadedStrings #-}
import Data.Aeson
import qualified Data.ByteString.Lazy.Char8 as BL
data Temperature = Temperature {celsius :: Double, city :: String}
deriving (Show)
instance ToJSON Temperature where
toJSON t = object ["city" .= city t, "celsius" .= celsius t, "fahrenheit" .= (celsius t * 9 / 5 + 32)]
instance FromJSON Temperature where
parseJSON = withObject "Temperature" $ \o -> do
c <- o .: "celsius"
name <- o .:? "city" .!= "unknown"
if c < -273.15
then fail "below absolute zero"
else return (Temperature c name)
main :: IO ()
main = do
BL.putStrLn (encode (Temperature 21.5 "Oslo"))
print (eitherDecode "{\"celsius\": 30}" :: Either String Temperature)
print (eitherDecode "{\"celsius\": -300}" :: Either String Temperature)
{"celsius":21.5,"city":"Oslo","fahrenheit":70.7}
Right (Temperature {celsius = 30.0, city = "unknown"})
Left "Error in $: below absolute zero"
.:reads a required field,.:?an optional one,.!=supplies a default.failinside a parser produces a decoding error with your message.
Use the JSON Formatter to pretty-print encoded output while debugging, and the JSON Validator to check sample payloads before writing a parser for them.