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 onApiResponse
? - is there a better alternative completely different from anything exposed above to address this?
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 aFromJSON
instance.Then, when you are writing the parser, you can use the parser for the type parameter as part of your definition. Like this:
Now, let's define two newtypes that borrow their respective
FromJSON
instances from that ofInt
, usingGeneralizedNewtypeDeriving
:If we load the file in ghci, we can supply the type parameter to
ApiResponse
and interrogate the available instances:You can also auto-derive
FromJSON
forApiResponse
, if you also deriveGeneric
: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 theGeneric
machinery, they need to use theanyclass
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: