用Aeson解析JSON时,为什么Maybe在类型参数中会被区别对待?

When parsing JSON with Aeson, why is Maybe treated differently when it's in a type parameter?

假设我们有一些数据类

{-# LANGUAGE DeriveGeneric, DuplicateRecordFields #-}

import Data.Aeson
import Data.ByteString.Lazy.Char8
import GHC.Generics

data Foo a = Foo { payload :: a }
    deriving (Show, Generic)

instance ToJSON a => ToJSON (Foo a)
instance FromJSON a => FromJSON (Foo a)

data Bar a = Bar { payload :: Maybe a }
    deriving (Show, Generic)

instance ToJSON a => ToJSON (Bar a)
instance FromJSON a => FromJSON (Bar a)

然后我们尝试解码如下:

*Main > decode $ pack "{}" :: Maybe (Bar String)
Just (Foo {payload = Nothing})
*Main > decode $ pack "{}" :: Maybe (Foo (Maybe String))
Nothing

那么为什么我们不能在最后一次尝试中解码 JSON?数据 类 似乎是一样的,它们都以相同的方式工作 toJSON:

*Main > toJSON $ Foo (Nothing :: Maybe String)
Object (fromList [("payload",Null)])
*Main > toJSON $ Bar (Nothing :: Maybe String)
Object (fromList [("payload",Null)])

更新:底部有一个简单的解决方案。

这令人困惑,但它或多或少按设计工作。您可以尝试将其作为 aeson 问题提交,但我怀疑它将作为 "won't fix".

关闭

发生的事情是为 FromJSON (Bar a) 生成的通用实例等同于:

instance FromJSON a => FromJSON (Bar a) where
  parseJSON = withObject "Bar" $ \v -> Bar
    <$> v .:? "payload"

请注意由于 Bar 中的 Maybe a 字段而生成的 (.:?) 运算符的使用。在混合了 Maybe 和非 Maybe 字段的结构中,相应地混合了 (.:?)(.:) 运算符。

请注意,此实例是为所有可能的 a 一劳永逸地生成的。它是多态的原因是 (.:?) 实现可以分派给实例约束提供的 FromJSON a 字典中的 parseJSON 方法。另请注意,我们可以使用 (.:?) 的唯一原因是在编译时已知对于所有可能的类型 aBar 对象中的字段 payload 具有类型 Maybe a 所以使用 (.:?) 运算符将进行类型检查。

现在,考虑为 FromJSON (Foo a) 生成的实例。这相当于:

instance FromJSON a => FromJSON (Foo a) where
  parseJSON = withObject "Foo" $ \v -> Foo
    <$> v .: "payload"

它与上面的 Bar a 实例完全相似,只是它使用了 (.:) 运算符。同样,它在编译时有一个单一的实现,通过分派到 FromJSON a 字典中的 parseJSON 来为每个可能的 a 工作。此实例无法使用 (.:?) 运算符,因为一般的 aMaybe t 无法统一,并且它无法以某种方式 "inspect" 类型 a,无论是在编译时还是运行时,查看它是否是 Maybe,原因与您不能编写类型为 a -> a 的完全多态函数的原因大致相同,除了身份.

因此,这个Foo a实例不能使payload字段可选!相反,它必须将 payload 视为强制性的,并且 - 当用于解析 Foo (Maybe String) 时 - 分派到 FromJSON t => FromJSON (Maybe t) 实例(允许 null 但否则分派到FromJSON String 个实例)。

现在,为什么它似乎对 ToJSON 有效?那么,ToJSON (Foo a)ToJSON (Bar a) 的实例都会生成相同类型的(单态)Value 表示:

> toJSON (Foo (Nothing :: Maybe String))
Object (fromList [("payload",Null)])
> toJSON (Bar (Nothing :: Maybe String))
Object (fromList [("payload",Null)])

并且当此值编码为 JSON.

时,将统一删除空字段

这导致 FromJSONToJSON 实例中出现不幸的不对称,但这就是正在发生的事情。

我才意识到我忘了分享修复它的简单解决方案。只需为 Foo 定义两个通用实例,一个重叠实例用于处理 Maybes,另一个用于其他类型:

instance {-# OVERLAPPING #-} FromJSON a => FromJSON (Foo (Maybe a))
instance FromJSON a => FromJSON (Foo a)