2

Toying a bit with the github gist API while trying to get down with the Aeson JSON library. I've run into a problem with the generated ToJSON instance, and I don't know exactly how to solve it.

I need to contain a value inside and the key that is associated to the value also needs to be a value and not a predefined key name. It's a bit easier to show. The desired output is,

{
    "public": true, 
    "description": "Something..", 
    "files": {"This Thing.md": {"content": "Here we go!"}}
}

where the value of the filename is holding the content, but currently I get,

{
    "public": true, 
    "description": "Something..", 
    "files": {"filename": "This Thing.md", "content": "Here we go!"}
}

Which isn't really what I need. The current code is,

{-# LANGUAGE OverloadedStrings, DeriveGeneric #-}
import Data.Text (Text)
import Data.Aeson
import GHC.Generics

data GistContent = GistContent
    { filename :: Text
    , content :: Text
      } deriving (Show, Generic)

instance ToJSON GistContent

data Gist = Gist
    { description :: Text
    , public      :: Bool
    , files       :: GistContent
      } deriving (Show, Generic)

instance ToJSON Gist

Under the assumption that it is possible, how would my datastructure need to look to get the desired output?.. And if that's not possible using the generics, how'd I got about it using the ToJSON instance (I can't quite figure out the structure there either)?

Tehnix
  • 2,020
  • 2
  • 18
  • 23

2 Answers2

3

Here's the manually written instance (see the documentation for the class):

instance ToJSON GistContent where
   toJSON (GistContent { filename = f, content = c }) = object [f .= c]

I doubt if there would be any way to get this with your existing datatype with the automatically generated instances because all they can do is to follow the datatype using a standard scheme. Note that you can still use the generic instance for Gist because that will call the (non-generic) instance for GistContent.

Ganesh Sittampalam
  • 28,821
  • 4
  • 79
  • 98
3

Your problem stems from an incorrect schema. files can currently only contain one GistContent, which is unnecessarily limiting. Instead, you'd want to have a list of GistContents:

data Gist = Gist
    { description :: Text
    , public      :: Bool
    , files       :: [GistContent]
    } deriving (Show, Generic)

Now consider another constraint on Gist: each GistContent must have a different filename. A data structure that would enforce this would be Data.HashMap.Strict.HashMap. Taking the filename out of GistContent and using the filename as a key:

data GistContent = GistContent
    { content :: Text
    } deriving (Show, Generic)

data Gist = Gist
    { description :: Text
    , public      :: Bool
    , files       :: HashMap Text GistContent
    } deriving (Show, Generic)

Everything works out.

icktoofay
  • 126,289
  • 21
  • 250
  • 231
  • Sweet, that did the trick :) Don't know why HashMaps never occurred to me, Aeson is still a bit of a stranger, but I'll hopefully get past that barrier pretty soon. – Tehnix Jan 26 '14 at 02:55
  • It may be instructive to look at the source for the `HashMap` instance (https://github.com/bos/aeson/blob/master/Data/Aeson/Types/Instances.hs#L422) - `HashMap` is actually used as the internal structure in Aeson objects (https://github.com/bos/aeson/blob/master/Data/Aeson/Types/Internal.hs#L171) which makes it particularly concise. – Ganesh Sittampalam Jan 26 '14 at 10:31
  • @icktoofay - did you mean `Data.HashMap.*Strict*.HashMap` above? – Ganesh Sittampalam Jan 26 '14 at 10:31
  • @Ganesh: I did, although both seem to work. Nice catch. – icktoofay Jan 26 '14 at 22:49