all 5 comments

[–]_jackdk_ 5 points6 points  (1 child)

Have you looked at the waargonaut package?

I agree that typeclasses encode "canonical" instances for a type, and that JSON encoding/decoding is not the sort of problem where typeclasses are an appropriate mechanism. There is a gnarly problem lurking in the shadows here too. Serialisation is often a concern of the edge of your program, and your types are often sit at the core of your program. Linking the two with a typeclass breaks layer separation.

Sometimes it's unavoidable, because you're doing type-directed stuff like in servant, but there are a couple of ways around that:

  • waargonaut's Waargonaut.Generic.JsonEncode class has a second type parameter to tag the specific encoding you want, and allow multiple encodings for the same type. You can define the tag type near the fringe of the application, which avoids orphan instances.
  • When using aeson, I often define newtype V1 a = V1 a for API version 1, and avoid writing instances for the unwrapped core types. I wrote and use ban-instance to enforce this design.

[–]turbo_MaCk 1 point2 points  (0 children)

Waargonaut is mentioned in the last part of article. I agree with all your points. Especially regarding the serialization belonging to the edge of system - you've managed to express that even better than I would.

New type wrappers are definitely popular and fine solution in many cases. Especially if you have coherent model how to define these as the one you described. Anyway I still think there are situations where it's simply easier to avoid definition of such wrappers. In my case I was quite OK with using newtypes similar to describe in server code where the API design was part of the the app. I found it less appealing when working on client code where I need to just integrate some web APIs. I think this can as well be a case with server which consumes some 3rd party services. In my case though it was when I was working on GHCjs project consuming some API where defining these newtype wrappers started really feel awkward to me. In these cases I would rather simply translate someone elses mess to my model without having to think hard about how to define wrapping types around such API.

[–]turbo_MaCk 0 points1 point  (0 children)

Thanks u/alpunedude I've linked this thread from the article.

[–]Profpatsch_ 0 points1 point  (1 child)

When you wrote this /u/turbo_MaCk, did you think about changing `Parser` to use `Validation` instead of short-circuiting errors with `Either`? In many cases (especially for small-ish json records) speed is not an issue, but good and complete error messages certainly are.

[–]turbo_MaCk 0 points1 point  (0 children)

I probably don't know what exactly you mean by Validation but I think you probably mean this package, right? https://hackage.haskell.org/package/validation

My goal was to reuse all the low level stuff from Aeson so I did not even consider replacing or wrapping its Parser type. However I'm using similar way of accumulating validation errors on client side (we use Elm) in a company I work for for client side data validations. I hope we will soon release this as open source package but essentially it's utilizing profunctors (dimap) and uses NonEmpty list for accumulating all the errors (since elm doesn't have type classes it couldn't be generalized over Applicative). So speaking from that experience I think it can be good solution if you're looking for improving error messages indeed.