0

I have an API which returns JSON results in the following form:

{
  "data": [1, 2, 3]
}

The data field can be the encoding of two distinct records which are shown below:

newtype ResultsTypeA = ResultsTypeA [ResultTypeA]
newtype ResultsTypeB = ResultsTypeB [ResultTypeB]

When I query this API from Haskell, I know in advance whether I'm dealing with a ResultsTypeA or a ResultsTypeB because I'm explicitly asking for it in the query.

The part where I'm struggling is with the Aeson ToJSON and FromJSON instances. Since both result types A and B are ultimately lists of Int, I can't use pattern matcher in FromJSON, because I could only match a [Int] in both cases.

This is why I thought of doing the following:

newType ApiResponse a =
    ApiResponse {
        data :: a
    }

newtype ResultsTypeA = ResultsTypeA [ResultTypeA]
newtype ResultsTypeB = ResultsTypeB [ResultTypeB]

However I can't get my head around how to write the ToJSON and FromJSON instances for the above, because now ApiResponse has a type parameter, and nowhere in Aeson docs seem to be a place where it is explained how to derive these instances with a type parameter involved.

Another alternative, avoiding a type parameter, would be the following:

newtype Results =
    ResultsTypeA [ResultTypeA]
  | ResultsTypeB [ResultTypeB]

newtype ApiResponse =
    ApiResponse {
        data :: Results
    }

In this case the ToJSON is straightforward:

instance ToJSON ApiResponse where
    toJSON = genericToJSON $ defaultOptions

But the FromJSON gets us back to the problem of not being able to decide between result types A and B...

It is also possible that I'm doing it wrong entirely and there is a third option I wouldn't see.

  • how would the FromJSON / ToJSON instances look like with a type parameter on ApiResponse?
  • is there a better alternative completely different from anything exposed above to address this?
Jivan
  • 21,522
  • 15
  • 80
  • 131
  • 1
    I'm not convinced it makes sense to have any Haskell types for this dedicated purpose _at all_. Sometimes it's the best to just work with [`Value`](https://hackage.haskell.org/package/aeson-1.5.6.0/docs/Data-Aeson.html#t:Value)s at the interface, and then extract the relevant data into Haskell types that don't bother approximating the JSON structure in any way. – leftaroundabout May 15 '21 at 12:26
  • 1
    I think you should be able to derive `FromJSON` and `ToJSON` for parameterized types just fine. The generated instance will be of the form `FromJSON a => FromJSON (ApiResponse a)`. You should also derive instances for `ResultsTypeA`, `ResultTypeA`, and so on. The ones for the `ResultTypeA` should piggyback on the underlying instances for `Int`. This can be done with `GeneralizedNewtypeDeriving` and `DerivingStrategies`: `deriving newtype (FromJSON, ToJSON)` – danidiaz May 15 '21 at 12:54
  • Frame challenge: what's wrong with just using `[Int]`? *Especially* given "When I query this API from Haskell, I know in advance whether I'm dealing with a `ResultsTypeA` or a `ResultsTypeB` because I'm explicitly asking for it in the query.". If you absolutely must have two distinct types, the querier can just apply the appropriate `newtype` constructor, but even that is, in most cases, just not needed. – Daniel Wagner May 15 '21 at 18:51

1 Answers1

2

Since both result types A and B are ultimately lists of Int, I can't use pattern matcher in FromJSON, because I could only match a [Int] in both cases.

If you have a parameterized type, and you are writing a FromJSON instance by hand, you can put the precondition that the parameter must itself have a FromJSON instance.

Then, when you are writing the parser, you can use the parser for the type parameter as part of your definition. Like this:

{-# LANGUAGE OverloadedStrings #-}
import Data.Aeson

data ApiResponse a =
    ApiResponse {
        _data :: a,
        other :: Bool
    } 

instance FromJSON a => FromJSON (ApiResponse a) where
    parseJSON = withObject "" $ \o -> 
          ApiResponse <$> o .: "data" -- we are using the parameter's FromJSON 
                      <*> o .: "other"

Now, let's define two newtypes that borrow their respective FromJSON instances from that of Int, using GeneralizedNewtypeDeriving:

{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE DerivingStrategies #-}
-- Make the instances for the newtypes exactly equal to that of Int
newtype ResultTypeA = ResultTypeA Int deriving newtype FromJSON
newtype ResultTypeB = ResultTypeB Int deriving newtype FromJSON

If we load the file in ghci, we can supply the type parameter to ApiResponse and interrogate the available instances:

ghci> :instances ApiResponse [ResultTypeA]
instance FromJSON (ApiResponse [ResultTypeA])

You can also auto-derive FromJSON for ApiResponse, if you also derive Generic:

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingStrategies #-}
import Data.Aeson
import GHC.Generics

data ApiResponse a =
    ApiResponse {
        _data :: a,
        other :: Bool
    } 
    deriving stock Generic
    deriving anyclass FromJSON

deriving stock Generic makes GHC generate a representation of the datatype's structure that can be used to derive implementations for other typeclasses—here, FromJSON. For those derivations to be made through the Generic machinery, they need to use the anyclass method.

The generated instance will be of the form FromJSON a => FromJSON (ApiResponse a), just like the hand-written one. We can check it again in ghci:

ghci> :set -XPartialTypeSignatures
ghci> :set -Wno-partial-type-signatures
ghci> :instances ApiResponse _
instance FromJSON w => FromJSON (ApiResponse w)
instance Generic (ApiResponse w) 
danidiaz
  • 26,936
  • 4
  • 45
  • 95