Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to serialise Haskell ADTs as tidy JSON, using Aeson?

I've spent some time playing around with Aeson, but I can't get Algebraic Data Types to serialise nicely.

What I've tried is:

data Attach = Attach { tel :: String }
              deriving (Show)
$(deriveJSON defaultOptions ''Attach)

data Fix = Fix { lat :: Double, lng :: Double }
              deriving (Show)
$(deriveJSON defaultOptions ''Fix)

data MsgIn = AttachMsg Attach
           | FixMsg    Fix
           deriving (Show)
$(deriveJSON defaultOptions ''MsgIn)

data MsgIn2 = MsgIn2 { attach :: Maybe Attach, fix :: Maybe Fix }
            deriving (Show)
$(deriveJSON defaultOptions ''MsgIn2)

someFunc :: IO ()
someFunc = do
  let attach = Attach "+447890"
  let reply = AttachMsg attach
  BL.putStrLn (encode reply)
  let reply2 = MsgIn2 (Just attach) Nothing
  BL.putStrLn (encode reply2)

The output is:

{"tag":"AttachMsg","contents":{"tel":"+447890"}}
{"attach":{"tel":"+447890"},"fix":null}

The output I'm looking for is:

{"attach":{"tel":"+447890"}}

but from the MsgIn type, rather than MsgIn2.

(The output of MsgIn2 gets quite close, but it's got an explicit null.)

Is there a way of doing this in Aeson?


Update:

I added:

instance ToJSON MsgIn3 where
  toJSON (AttachMsg3 (Attach tel)) = object ["attach" .= object ["tel" .= tel]]
...
let reply3 = AttachMsg3 attach
BL.putStrLn (encode reply3)

and got the answer I wanted: {"attach":{"tel":"+447890"}}.

@bheklilr is there a way to use Attach's (already defined) serialisation, instead of defining it again?

I've tried some nonsense syntax, but understandably it doesn't compile:

instance ToJSON MsgIn3 where
  toJSON (AttachMsg3 (Attach tel)) = object ["attach" .= (toJSON :: Attach)] 
like image 222
fadedbee Avatar asked Jul 15 '15 20:07

fadedbee


2 Answers

Use custom options instead of defaultOptions. You can get the right structure by using sumEncoding = ObjectWithSingleField, which reduces your first example to {"AttachMsg":{"tel":"+447890"}}. You can then customize the constructor tags by using constructorTagModifier = myConstructorTag and by writing a function myConstructorTag that customizes the names to your liking (e.g. AttachMsg -> attach).

As an example, you'll get the output you want by writing this into a separate module, importing it, and using myOptions instead of defaultOptions:

myConstructorTag :: String -> String
myConstructorTag "AttachMsg" = "attach"
myConstructorTag x = x

myOptions :: Options
myOptions = defaultOptions {sumEncoding = ObjectWithSingleField, constructorTagModifier = myConstructorTag}

A separate module is needed here because of Template Haskell. There's probably a way to define myConstructorTag and myOptions in a better way to satisfy the needs of TH, but I have absolutely no idea how to do that.

like image 183
gekkio Avatar answered Nov 06 '22 19:11

gekkio


You can get aeson to skip null fields automatically. I usually do this in combination with the DeriveGeneric extension:

{-# LANGUAGE OverloadedStrings, DeriveGeneric #-}

import Data.Aeson
import Data.Aeson.Types
import qualified Data.ByteString.Lazy.Char8 as BL
import GHC.Generics

data Attach = Attach { tel :: String } deriving (Show, Generic)

data Fix = Fix { lat :: Double, lng :: Double } deriving (Show, Generic)

data Msg = Msg { attach :: Attach, fix :: Maybe Fix } deriving (Show, Generic)

instance ToJSON Attach
instance ToJSON Fix
instance ToJSON Msg where
    toJSON = genericToJSON (defaultOptions { omitNothingFields = True })

main = do
    let attach = Attach "+447890"
        reply  = Msg attach Nothing
    BL.putStrLn (encode reply)

which gives you:

*Main> main
{"attach":{"tel":"+447890"}}
like image 25
Shaun the Sheep Avatar answered Nov 06 '22 18:11

Shaun the Sheep