在同一字段中使用两种不同的可能类型派生 Aeson 类型类

问题描述

我有一个 API,它以以下形式返回 JSON 结果:

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

data 字段可以是两个不同记录的编码,如下所示:

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

当我从 Haskell 查询这个 API 时,我事先知道我是在处理 ResultsTypeA 还是 ResultsTypeB,因为我在查询中明确要求它。

我遇到困难的部分是 Aeson ToJSONFromJSON 实例。由于结果类型 AB 最终都是 Int 的列表,因此我无法在 FromJSON 中使用模式匹配器,因为我只能匹配 [Int]在这两种情况下。

这就是我想到做以下事情的原因:

newType ApiResponse a =
    ApiResponse {
        data :: a
    }

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

但是我无法理解如何编写上述的 ToJSONFromJSON 实例,因为现在 ApiResponse一个类型参数,而 Aeson 文档中似乎没有任何地方是解释如何派生这些涉及类型参数的实例的地方。

另一种避免类型参数的替代方法如下:

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

newtype ApiResponse =
    ApiResponse {
        data :: Results
    }

在这种情况下,ToJSON 很简单:

instance ToJSON ApiResponse where
    toJSON = genericToJSON $ defaultOptions

但是 FromJSON 让我们回到无法在结果类型 AB 之间做出决定的问题...

也有可能我完全做错了,还有第三种我看不到的选择。

  • 如果 FromJSON 上有类型参数,ToJSON / ApiResponse 实例会是什么样子?
  • 是否有更好的替代方案来解决这个问题?

解决方法

由于结果类型 A 和 B 最终都是 Int 列表,我不能在 FromJSON 中使用模式匹配器,因为在这两种情况下我只能匹配 [Int]。

如果你有一个参数化的类型,并且你正在手工编写一个 FromJSON 实例,你可以把参数本身必须有一个 FromJSON 实例作为前提。

然后,在编写解析器时,您可以将类型参数的解析器用作定义的一部分。像这样:

{-# 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"

现在,让我们定义两个新类型,它们使用 FromJSON 借用各自的 Int 实例:

GeneralizedNewtypeDeriving

如果我们在 ghci 中加载文件,我们可以为 {-# 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 interrogate the available instances 提供类型参数:

ApiResponse

您还可以为 ghci> :instances ApiResponse [ResultTypeA] instance FromJSON (ApiResponse [ResultTypeA]) 自动推导 FromJSON,如果您还推导 ApiResponse

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 使 GHC 生成数据类型结构的表示,可用于派生其他类型类的实现——这里是 deriving stock Generic。对于通过 FromJSON 机制进行的派生,他们需要使用 anyclass 方法。

生成的实例将采用 Generic 的形式,就像手写的一样。我们可以在 ghci 中再次检查:

FromJSON a => FromJSON (ApiResponse a)