Aeson 中的单个标记构造函数



>我有这样的数据类型:

data A = A T.Text deriving (Generic, Show)
instance A.ToJSON A 

如果我使用它A.encode

A.encode $ A "foobar" -- "foobar"

然后我在上面使用singleTagConstructors

instance A.ToJSON A where
toEncoding a = A.genericToEncoding $ A.defaultOptions { A.tagSingleConstructors = True }
A.encode $ A "foobarquux" -- "{tag: A, contents: foobarquux}"

在某个时候,我创建了另一种数据类型:

newtype Wrapper a = Wrapper 
{ unWrap :: a
} deriving (Show)
instance A.ToJSON a => A.ToJSON (Wrapper a) where 
toJSON w = A.object [ "wrapped" A..= unWrap w ]

这是我感到困惑的部分:

A.encode $ Wrapper $ A "foobar" -- "{wrapped: foobar}"

我如何得到这样的结果?

"{wrapped: {tag: A, contents: foobarquux}}"

要直接回答这个问题,你总是可以使用tagSingleConstructors = False实现Wrapper实例,如下所示:

instance Generic a => A.ToJSON (Wrapper a) where 
toJSON w = A.object [ "wrapped" A..= encA (unWrap w) ]
where
encA = A.genericToEncoding $ A.defaultOptions { A.tagSingleConstructors = False }

但我不明白你为什么要这样做。

如果您控制 API,则不需要tag字段:包装值的预期类型已经是静态已知的,因此tag没有帮助。

如果您不控制 API,我建议您非常明确地表示它,例如作为与 API 形状完全匹配的记录。否则,您将面临在代码库的远程部分进行不相关更改而意外破坏 API 的风险。

问题在于您如何实现自定义ToJSON实例。

instance A.ToJSON A where
toEncoding a = A.genericToEncoding $ A.defaultOptions { A.tagSingleConstructors = True }

由于您没有实现toJSON因此直接使用类型类定义中的默认实现。

class ToJSON a where -- excerpt from Data.Aeson.Types.ToJSON
-- | Convert a Haskell value to a JSON-friendly intermediate type.
toJSON     :: a -> Value
default toJSON :: (Generic a, GToJSON' Value Zero (Rep a)) => a -> Value
toJSON = genericToJSON defaultOptions

实际上,您有以下实例:

instance A.ToJSON A where
toJSON = genericToJSON defaultOptions
toEncoding a = A.genericToEncoding $ A.defaultOptions { A.tagSingleConstructors = True }

虽然toEncoding使用预期的标记编码,但toJSON使用默认编码(不标记单个构造函数)。这种不一致是造成混淆的根本原因。稍后,在包装器的ToJSON实例中使用了运算符.=。在内部,它使用toJSON而不是toEncoding

class KeyValue kv where
(.=) :: ToJSON v => Text -> v -> kv
infixr 8 .=
instance KeyValue Pair where
name .= value = (name, toJSON value)

作为解决方案,您应该仅定义toJSON并保留默认toEncoding实现(使用toJSON),或者同时实现两者。

相关内容

最新更新