>我有这样的数据类型:
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
),或者同时实现两者。