aeson
aeson copied to clipboard
Irregular Aeson.TH results with ObjectWithSingleField
This might be a documentation issue. The documentation for ObjectWithSingleField says:
A constructor will be encoded to an object with a single field named after the constructor tag (modified by the constructorTagModifier) which maps to the encoded contents of the constructor.
Here's my test program:
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE CPP #-}
module Main where
import Language.Haskell.Exts.Annotated
import Data.Aeson
import Data.Aeson.TH
$(deriveToJSON defaultOptions { sumEncoding = ObjectWithSingleField } ''ExportSpecList)
$(deriveToJSON defaultOptions { sumEncoding = ObjectWithSingleField } ''SrcSpanInfo)
$(deriveToJSON defaultOptions { sumEncoding = ObjectWithSingleField } ''SrcSpan)
Running ghc -ddump-splices --make Main.hs
gives (1/3):
Main.hs:1:1: Splicing declarations
deriveToJSON
(defaultOptions {sumEncoding = ObjectWithSingleField}) ''SrcSpan
======>
Main.hs:25:3-79
instance ToJSON SrcSpan where
toJSON
= \ value_a81J
-> case value_a81J of {
SrcSpan arg1_a81K arg2_a81L arg3_a81M arg4_a81N arg5_a81O
-> object
[((Data.Text.pack "srcSpanFilename") .= arg1_a81K),
((Data.Text.pack "srcSpanStartLine") .= arg2_a81L),
((Data.Text.pack "srcSpanStartColumn") .= arg3_a81M),
((Data.Text.pack "srcSpanEndLine") .= arg4_a81N),
((Data.Text.pack "srcSpanEndColumn") .= arg5_a81O)] }
An object, and a set of values that are of primitive types so no need to toJSON
them. Ok, SrcSpanInfo next:
Main.hs:1:1: Splicing declarations
deriveToJSON
(defaultOptions {sumEncoding = ObjectWithSingleField})
''SrcSpanInfo
======>
Main.hs:24:3-83
instance ToJSON SrcSpanInfo where
toJSON
= \ value_a80U
-> case value_a80U of {
SrcSpanInfo arg1_a80V arg2_a80W
-> object
[((Data.Text.pack "srcInfoSpan") .= arg1_a80V),
((Data.Text.pack "srcInfoPoints") .= arg2_a80W)] }
An object, and a set of values that are not toJSON'd. The values are of type SrcSpan and [SrcSpan]. Ok, ExportSpecList next:
Main.hs:1:1: Splicing declarations
deriveToJSON
(defaultOptions {sumEncoding = ObjectWithSingleField})
''ExportSpecList
======>
Main.hs:12:3-86
instance ToJSON l_a6O3 => ToJSON (ExportSpecList l_a6O3) where
toJSON
= \ value_a7Us
-> case value_a7Us of {
ExportSpecList arg1_a7Ut arg2_a7Uu
-> Array
(Data.Vector.create
(do { mv_a7Uv <- Data.Vector.Mutable.unsafeNew 2;
Data.Vector.Mutable.unsafeWrite mv_a7Uv 0 (toJSON arg1_a7Ut);
Data.Vector.Mutable.unsafeWrite mv_a7Uv 1 (toJSON arg2_a7Uu);
return mv_a7Uv })) }
An array and a set of values that are toJSON'd.
Here are the datatypes cut and pasted from the Source-link on Hackage:
-- | A portion of the source, spanning one or more lines and zero or more columns.
data SrcSpan = SrcSpan
{ srcSpanFilename :: String
, srcSpanStartLine :: Int
, srcSpanStartColumn :: Int
, srcSpanEndLine :: Int
, srcSpanEndColumn :: Int
}
deriving (Eq,Ord,Show,Typeable,Data)
data SrcSpanInfo = SrcSpanInfo
{ srcInfoSpan :: SrcSpan
-- , explLayout :: Bool
, srcInfoPoints :: [SrcSpan] -- Marks the location of specific entities inside the span
}
deriving (Eq,Ord,Show,Typeable,Data)
-- | An explicit export specification.
data ExportSpecList l
= ExportSpecList l [ExportSpec l]
deriving (Eq,Ord,Show,Typeable,Data,Foldable,Traversable,Functor,Generic)
I might be doing unsupported things, but in that case it would be nice to get a warning that I can't expect to deserialize my values in a regular way on the receiving end.
I'm using ghc 7.6.3, haskell-src-exts 1.15.0.1, and aeson 0.7.0.6 if that makes any difference.
What encoding did you expect from reading the documentation? Maybe I can improve it.
Do note that .= calls toJSON internally. Also note that the sumEncoding only applies to sum data types i.e. multiple constructors.
I was expecting the sum-encoding to win on walk-over for this case since:
-
A
could be seen as a degenerate form ofA + B + ..
- There is nothing else in defaultOptions that seems even remotely relevant for how ExportSpecList is encoded in my test program.
I'm not that picky about which particular encoding I use to be honest. I want something that I can easily read with Gson without being insanely space-inefficient.
A could be seen as a degenerate form of A + B + ..
Single constructor types are currently not tagged with the constructor tag since that would be redundant.
Having said that, I would accept a patch that adds a configuration option which when toggled also applies the sumEncoding to non-sum types. Note that this has to work the same for both the TH and GHC Generics encoders.
There is nothing else in defaultOptions that seems even remotely relevant for how ExportSpecList is encoded in my test program.
A non-record product is currently encoded to an array of the product's arguments. What encoding did you expect for non-record product types like ExportSpecList? Could you provide a manual ToJSON instance so that I can see what you need?
I'm afraid my TH-fu is not sufficient to provide any patches; I was delighted when I found that all this could be automated since there's quite a few data types to serialize in haskell-src-exts.
What encoding did you expect for non-record product types like ExportSpecList?
I was expecting a single field object where the field was named after the constructor, and that field would have an array value consisting of the encoded parameters. It might be more constructive to describe the end goal though--here's what I want to deserialize into with Gson:
public class ExportSpecList {
public SrcSpanInfo srcSpanInfo;
public ExportSpec[] exportSpec;
}
The class structure isn't set in stone, but it would be nice if there's a reasonable (mechanical) translation between the generating Haskell side and the parsing Java side.
I think the following would be best:
- We rename the
sumEncoding
field toconstructorEncoding
. - We rename the
SumEncoding
type toConstructorEncoding
. - We add a
Bool
fieldencodeSingleConstructors
(name up for bikeshedding) which whenTrue
also applies theconstructorEncoding
to single constructor types. WhenFalse
it keeps the current "walk-over" behavior. -
constructorEncoding
defaults toFalse
indefaultOptions
to make the encoding backwards compatible.
I currently don't have time to implement this. My next goal for aeson is making the encoder more efficient by using a deep-embedding of the JSON encoder (same trick as used in the upcoming binary package). This is already mostly implemented in my json-builder branch. I hope to finish that at ZuriHac 2014. After that I can look at this if nobody beats me to it.