Aeson 中的单标签构造函数

Single tag constructors in 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 使用默认编码(不标记单个构造函数)。这种不一致是造成混乱的根本原因。后来,在 wrapper 的 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),或者同时实现两者。