1
votes

I have data which is a map. To make the question more concrete, let's think that it's represented as an assoc-list type D val = [(Key,val)] (or as type D val = Map Key val).

  • Key is an "enum" type -- a sum of nullary constructors, e.g.:

    data Key = C1 | C2 | C3

  • There must be instance ToJSON for val.

I'd like to implement

instance ToJSON val => ToJSON (D val)

which would be like instance Map String val (producing an object with the corresponding key-value pairs) (an example, another example for HashMaps), only more generic.

More generic means that I'm not restricted to String or Text as the key (such instances for Map are found in the aeson package).

I want to learn how to use the generic mechanisms in aeson to do this concisely.

aeson already can convert "enums" to JSON strings, and also (C val1 val2 ...) to objects with a single key (the constructor name, like this I assume: { "C":[val1,val2,...] }) with genericToJSON defaultOptions{ sumEncoding = ObjectWithSingleField }. And another conversion which uses the constructor names is with the default options for data types with non-nullary constructors: then the constructor name becomes the value of a "tag" key. I could use similar code.

I think that even such an instance (for maps whose keys are from enums) would be useful addition to the library. But AFAIU neither aeson nor generic-aeson (which has adds some nicer conversion options) has such a conversion predefined.

1
"I think that even such an instance (for maps whose keys are from enums) would be useful addition to the library" -> I've never needed that instance, your use case seems fairly specific, I'm not sure it would be particularly useful to have defined in the aeson package. The real problem I see here is converting Map Key val to Map Text val, then outputting that. Why not just write a function that converts your keys to a type that already instances ToJSON instead of worrying about generic programming?bheklilr
@bheklilr I like that the constructor names from Haskell are re-used as key names. I actually plan to vary this enum when working on my project, so I don't want to maintain the functions for Text <-> Key conversions (the other direction might be needed for FromJSON; well, not necessarily, because aeson can live with single Options for both directions of conversion, having a single fieldLabelModifier for both direections, and I see how this can be done for an enum.)imz -- Ivan Zakharyaschev
@bheklilr I felt that this is somehow in spirit of Aeson to represent many native Haskell data types as JSON without relying on user-supplied conversions to text. I find this joyful.imz -- Ivan Zakharyaschev
@bheklilr Re "Why not just write a function that converts your keys to a type that already instances ToJSON instead of worrying about generic programming?" I assume the keys do instance ToJSON, that's not a problem for me. But that wouldn't automatically (or with some options) give me the desired JSON representation. Yes, with a "hack" I can really write a short implementation of what I want (my answer below) relying on the instance ToJSON Key, but that's ugly. The generic bit of code that works in such default instance ToJSON Key could be directly used here, instead of the non-total λ.imz -- Ivan Zakharyaschev
Using a record instead of a map (where the fields are instead of my enum type constructors) is not something convenient for me because it doesn't allow to do computations on the Key data.imz -- Ivan Zakharyaschev

1 Answers

1
votes

A quick and short, but ugly solution is:

instance (ToJSON a) => ToJSON (D a)
    where toJSON kvs = object [ t .= v | (k,v) <- kvs, let (String t) = toJSON k ]

Advantages:

  • it re-uses the generic mechanisms of aeson to get the key representation (as text);
  • it would re-use the same conversion rules for the names of the fields here as defined in the instance ToJSON Key, so we don't need to write them twice, or to care about consistency.

Disadvantages:

  • converting first to Object just to extract a String is simply ugly;
  • it introduces a dangerous non-total conversion (which even may fail if toJSON for Key is implemented in a non-default way;
  • there is no compile-time check that Key is an enum relying on types. (Actually, I'm not very keen on understanding the generics type machinery yet, so I see that such checks and choices can be done because aeson is implemented this way, but I wouldn't be able to write this myself yet.)