防止 Aeson parseJSON 中的未知字段名称

问题描述

具有以下类型和实例派生:

{-# LANGUAGE RecordWildCards #-}

import           Data.Aeson
import           Data.Text

data MyParams = MyParams {
    mpFoo :: Maybe Text,mpBar :: Maybe Text
} deriving Show

instance FromJSON MyParams where
    parseJSON = withObject "MyParams" $ \q -> do
        mpFoo <- q .:? "foo"
        mpBar <- q .:? "bar"
        pure MyParams {..}

如何确保以下 JSON 会失败?

{
  "foo": "this is a valid field name","baa": "this is an invalid field name"
}

通过上面的代码,这个 JSON 成功了,因为 1. bar 是可选的,所以 parseJSON 如果没有找到它不会抱怨,并且 2. baa 不会抛出任何错误但是反而会被忽略。 (1) 和 (2) 的组合意味着无法捕获字段名称中的拼写错误并且会被静接受,尽管会生成错误的结果 (MyParams { foo = Just(this is a valid field name),bar = nothing })。

事实上,这个 JSON 字符串也应该失败:

{
  "foo": "this is fine","bar": "this is fine","xyz": "should trigger failure but doesn't with the above code"
}

TL;DR:当 JSON 包含任何与 parseJSONfoo 不匹配的字段名称时,如何使 bar 失败?

解决方法

不要忘记您在 q 中可以访问的 withObject 只是一个 HashMap。所以,你可以写:

import qualified Data.HashMap.Strict as HM
import qualified Data.HashSet as HS
import Control.Monad (guard)

instance FromJSON MyParams where
    parseJSON = withObject "MyParams" $ \q -> do
        mpFoo <- q .:? "foo"
        mpBar <- q .:? "bar"
        guard $ HM.keysSet q `HS.isSubsetOf` HS.fromList ["foo","bar"]
        pure MyParams {..}

这将保证json只有至多元素"foo""bar"

但是,考虑到 aeson 免费为您提供所有这些,这确实有点过分。如果您可以导出 Generic,那么您只需调用 genericParseJSON,如下所示:

{-# LANGUAGE DeriveGeneric #-}

data MyParams = MyParams {
    mpFoo :: Maybe Text,mpBar :: Maybe Text
} deriving (Show,Generic)

instance FromJSON MyParams where
  parseJSON = genericParseJSON $ defaultOptions
    { rejectUnknownFields = True,fieldLabelModifier = map toLower . drop 2
    }

这里我们通过两种方式调整默认解析选项:首先,我们告诉它拒绝未知字段,这正是您所要求的,其次,我们告诉它如何从字段名称 "foo"(对于 "mpFoo" 也是如此)。