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

Prevent unknown field names in 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 包含与 foo 或 [=13 不匹配的任何字段名称时,我如何使 parseJSON 失败=]?

不要忘记您在 withObject 中可以访问的 q 只是一个 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"(对于 bar 也是如此)。