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)]
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.
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"}}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With