5

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)] 
fadedbee
  • 42,671
  • 44
  • 178
  • 308
  • 2
    You could just write the `ToJSON` and `FromJSON` instances manually to get exactly what you want: `toJSON (AttachMsg (Attach tel)) = object ["attach" .= object ["tel" .= tel]]`, and similarly for `FixMsg`. The `parseJSON` implementation wouldn't be much more difficult. – bheklilr Jul 15 '15 at 20:19
  • @bheklilr Thanks, that was really useful. Could you make it into an answer? Could you see my updated question... – fadedbee Jul 15 '15 at 20:43

2 Answers2

5

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.

gekkio
  • 171
  • 3
4

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"}}
Shaun the Sheep
  • 22,353
  • 1
  • 72
  • 100
  • Thanks for your answer, but I didn't explain myself clearly enough. I want the output `{"attach":{"tel":"+447890"}}` from the original `MsgIn` data type. – fadedbee Jul 15 '15 at 20:56
  • 1
    This is still useful info, even if it doesn't answer the question, so I'm upvoting it anyway. – Carl Jul 15 '15 at 21:33