2
votes

I want to output my application's logs in JSON, but there are some ubiquitous data-types for which ToJSON instances are not defined - most notably SomeException and the entire Exception hierarchy of types.

I have two choices:

  1. Define instances of ToJSON for such data-types in my application
  2. Write my own type-class, say ToJsonLogs, and make it reuse ToJSON instances as much as possible.

The first is the path of "least resistance" but it has other implications. Since type-class instances are global in nature, I might end-up defining ToJSON instances that break something. Also, for the same data-structure, I might want the JSON in APIs to be different from the JSON in logs (for example, scrubbing keys, auth-tokens, and other sensitive data OR truncating very long text fields).

This questions is about exploring the second option. How do I go about doing something like the following:

class ToJsonLogs a where
  toJsonLogs :: a -> Aeson.Value

  default toJsonLogs :: (ToJSON a) => a -> Aeson.Value
  toJsonLogs = toJSON

instance ToJsonLogs SomeException where
  toJsonLogs = toJSON . displayException

I tried the above idea, but it failed at the very first step itself. Here's an example data-structure:

data SyncResult = SyncResult
  { resAborted :: !Bool
  , resSuccessful :: !Int
  , resFailed :: ![(Int, SomeException)]
  } deriving (Show)

I can't derive ToJsonLogs without first deriving ToJSON for the entire data-structure. Derivation of ToJSON fails because of SomeException. Hence the title of this question.

I even tried fooling around with Generics, but as usual, got stuck again.

1
Another possibility is to use wrapped versions of the instance-less types, which have your custom instances. resFailed :: [(Int, MyException)] where MyException is a newtype wrapper around SomeExceptionluqui

1 Answers

0
votes

You are very close to a possible extension-free solution. The thing you should consider is to create a wrapper for the original ToJson class members:

class ToJsonLogs a where
  toJsonLogs :: a -> Aeson.Value

newtype WrapToJson a = WrapToJson a  -- actually an Identity

instance ToJson a => ToJsonLogs (WrapToJson a) where
  toJsonLogs (WrapToJson x) = toJson x

-- example
logInt :: Int -> Aeson.value
logInt x = toJsonLogs (WrapJson x)

If you want to restrict the wrapper only for ToJson instances, you will need to enable few extensions:

{-# LANGUAGE GADTSyntax, ExistentialQuantifiaction #-}
data WrapToJson a where WrapToJson :: ToJson a => a -> WrapToJson a  

If you don't enjoy this wrapper, you may hide it under another definition of toJsonLogs:

toJsonLogs' :: ToJson a => a -> Aeson.value
toJsonLogs' = toJsonLogs . WrapToJson