With generics, the user defines a data type which will inherit some operation (e.g. toJSON
) based on the operation defined over its generically derivered structure. It doesn't create new types based on old. If you are looking for Genrics to create new types based on old, you will be frustrated.
More pointedly, the statement "I'd like to write a Generic instance for Aggregate" doesn't make sense. We make Generic representations instances of classes (class ToJSON
), not data structures. And the generic JSON instances are already written...
Luckily, a good solution may be easy. You just need instead a way work with collections of JSON objects. Below I demonstrate how to combine two data types that have JSON.Object
representations. Since a JSON.Object
is just a hashmap, which can be combined via union
, I just convert my haskell values to objects and perform a union. There is room for improvement, but this is the idea.
import qualified Data.Aeson as JSON
import qualified Data.Aeson.Types as JSON
import GHC.Generics
import qualified Data.HashMap.Lazy as HashMap
data A = A { fieldA :: String } deriving (Show,Eq,Generic)
data B = B { fieldB :: String } deriving (Show,Eq,Generic)
instance ToJSON A
instance ToJSON B
instance FromJSON A
instance FromJSON B
toObj :: ToJSON a => a -> Maybe JSON.Object
toObj = JSON.parseMaybe parseJSON . toJSON
toJSONAB :: A -> B -> Maybe JSON.Value
toJSONAB a b = do
aObj <- toObj a
bObj <- toObj b
return . JSON.Value $ HashMap.union aObj bObj
At this point, you would call toJSONAB
instead of toJSON
when you need to output JSON. Retrieving either an A or B from the output is included. That is,
(toJSONAB a b >>= JSON.parseMaybe parseJSON) :: Maybe <Desired Type>
will parse either an A or a B, depending on the type signature supplied (deduced).
The above is the core of what you want. You can make functions like this for whatever data combinations you like. What's missing is the creation of new types like below:
data AB = AB A B
which lend type safety to your code. Afterall, you are interested in specific types, not an ad-hoc collection of JSON representations of types. To do this via Generics, I suggest a new class such as below (untested and incomplete),
class ToFlatJSON a where
toFlatJSON :: a -> JSON.Value
default toFlatJSON :: (Generic a, GToFlatJSON (Rep a)) => a -> JSON.Value
toFlatJSON = gToFlatJSON . from
class GToFlatJSON a where
gToFlatJSON :: a p -> JSON.Value
and provide GToFlatJSON
instances for any necessary Generic representations found in GHC.Generics
.
instance (ToJSON a) => GToFlatJSON (K1 i a) where
gToFlatJSON (K1 a) = toJSON a
instance (GToFlatJSON a) => GToFlatJSON (a :*: a') where
gToFlatJSON (a :*: a') = cmb (gToFlatJSON a) (toFlatJSON a')
where
cmb = someFunctionLike_toJSONAB
instance (GToFlatJSON a) => GToFlatJSON (M1 t i a) where
gToFlatJSON (M1 _ _ a) = gToFlatJSON a
where
cmb = someFunctionLike_toJSONAB
Then, you will be able to define blank ToFlatJSON
instances like you do with ToJSON
instance ToFlatJSON a
and use toFlatJSON
instead of toJSON
. You could define toJSON
in terms toFlatJSON
. For each data type, then, you would need:
instance ToFlatJSON AB
instance ToFlatJSON AB => ToJSON AB where
toJSON = toFlatJSON
So, in summary, you can easily make your combination type by working with the JSON representations themselves, i.e. a union of their object representations. You can recover your original types directly using their fromJSON
. There's no way to overload the To/FromJSON
generic instances, but you could make a new similar class and its generic instances. I personally recommend for this application to stay away from the generics. I think custom To/FromJSON for your data types would be the most direct method.