Reputation: 23038
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.
FromJSON
/ ToJSON
instances look like with a type parameter on ApiResponse
?Upvotes: 0
Views: 481
Reputation: 27771
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)
Upvotes: 2