简体   繁体   中英

Deriving Aeson type classes with two different possible types in the same field

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?

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) 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM