使用Aeson解析JSON时,为什么将其放在类型参数中可能会有所不同?

voz*_*man 5 haskell aeson

假设我们有一些数据类

{-# 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)
Run Code Online (Sandbox Code Playgroud)

然后我们尝试解码如下:

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

那么,为什么不能在上一次尝试中解码JSON?数据类似乎是相同的,并且它们的使用方式都相同toJSON

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

K. *_*uhr 5

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

这很令人困惑,但是它或多或少地按设计工作。您可以尝试将其作为aeson问题提交,但我怀疑它将由于“无法解决”而被关闭。

发生的是,为生成的通用实例FromJSON (Bar a)等效于:

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

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

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

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

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

Bar a除了使用(.:)运算符之外,它与上面的实例完全相似。同样,它在编译时只有一个实现,可以a通过parseJSONFromJSON a字典中分派到来实现所有可能。(.:?)由于通用aMaybe t无法统一,因此该实例无法使用运算符,并且无论如何,它都无法以某种方式“检查”类型a(无论是在编译时还是在运行时)以查看其是否是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)])
Run Code Online (Sandbox Code Playgroud)

当此值编码为JSON时,null字段的删除会统一进行。

不幸的是FromJSON,这导致了和ToJSON实例的不对称,但这就是正在发生的事情。

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

instance {-# OVERLAPPING #-} FromJSON a => FromJSON (Foo (Maybe a))
instance FromJSON a => FromJSON (Foo a)
Run Code Online (Sandbox Code Playgroud)