Search code examples
haskellaeson

Making ToJSON use Show Instance


If I have a data type that looks like this:

data SumType = ABC | DEF deriving (Generic, ToJSON)
data MyType = MyType {field1 :: String, field2 :: SumType} deriving (Generic, ToJSON) 

The above will generate a JSON that looks like: {"field1": "blah", "field2":"ABC"}

In practice, MyType is a fairly complex type and I would like to keep the ToJSON deriving but want to adjust just one field to use the show instance.

 instance Show SumType where
   show ABC = "abc-blah"
   show DEF = "def-xyz" 

Unfortunately, the above Show instance does not get picked up by ToJSON (I don't know if it is supposed to). Hand rolling ToJSON for SumType does not seem to work because it expects a key-value pair (Maybe there is another of way doing it?). In other words, the JSON will be like: {"field1": "blah", "field2":{"field3": "ABC"}} -- I just want to change the way the value is stringified and not create a new object there.

Any suggestions on how I can change the output string of SumType without manually creating ToJSON for MyType? So the output is {"field1": "blah", "field2":"abc-blah"}

Thanks!


Solution

  • I do not see what the problem is with defining a ToJSON instance for the SumType. You can do this with:

    import Data.Aeson(ToJSON(toJSON), Value(String))
    import Data.Text(pack)
    
    instance ToJSON SumType where
        toJSON = String . pack . show
    

    Or if you want to use other strings for the ToJSON than Show:

    {-# LANGUAGE OverloadedStrings #-}
    
    import Data.Aeson(ToJSON(toJSON), Value(String))
    
    instance ToJSON SumType where
        toJSON ABC = String "ABC for JSON"
        toJSON DEF = String "DEF for JSON"
    

    Now Haskell will JSON-encode the SumType as a JSON string:

    Prelude Data.Aeson> encode ABC
    "\"ABC for JSON\""
    Prelude Data.Aeson> encode DEF
    "\"DEF for JSON\""
    

    You can do the same with FromJSON to parse the JSON string back into a SumType object:

    {-# LANGUAGE OverloadedStrings #-}
    
    import Data.Aeson(FromJSON(parseJSON), withText)
    
    instance FromJSON SumType where
        parseJSON = withText "SumType" f
            where f "ABC for JSON" = return ABC
                  f "DEF for JSON" = return DEF
                  f _ = fail "Can not understand what you say!"
    

    If we then parse back the JSON string, we get:

    Prelude Data.Aeson> decode "\"ABC for JSON\"" :: Maybe SumType
    Just ABC
    Prelude Data.Aeson> decode "\"DEF for JSON\"" :: Maybe SumType
    Just DEF
    Prelude Data.Aeson> decode "\"other JSON string\"" :: Maybe SumType
    Nothing
    Prelude Data.Aeson> decode "{}" :: Maybe SumType
    Nothing
    

    So in case we do not decode a JSON string that follows one of the patterns we have defined, the parsing will fail. The same happens if we do not provide a JSON string, but for instance an empty JSON object.

    Additional notes:

    1. Since SumType here has two values, you can also use a JSON boolean to encode the values.
    2. you can also encode on different JSON objects. You can for instance use the JSON string for ABC, and an integer for DEF. Although I would advice not to do this until there are good reasons (if for instance ABC carries only a string, and DEF ony an integer).
    3. usually the more complex you make encoding, the more complex decoding will be.