3

With the following type and instance deriving:

{-# 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 {..}

How can I make sure that the following JSON would fail?

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

With the code above, this JSON succeeds because 1. bar is optional, so parseJSON doesn't complain if it doesn't find it, and 2. baa will not throw any error but will instead be ignored. The combination of (1) and (2) means that typos in field names can't be caught and will be silently accepted, despite generating an incorrect result (MyParams { foo = Just(this is a valid field name), bar = Nothing }).

As a matter of fact, this JSON string should also fail:

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

TL;DR: how can I make parseJSON fail when the JSON contains any field name that doesn't match either foo or bar?

Jivan
  • 21,522
  • 15
  • 80
  • 131
  • Related: https://stackoverflow.com/questions/61928833/better-ways-to-collect-all-unused-field-of-an-object-in-aesons-parser – danidiaz May 03 '21 at 20:58
  • `parseJSON` is a function that goes `Value -> Parser whatever`. You can just... implement whatever checks you want inside it. You just Write Code. – Daniel Wagner May 03 '21 at 23:06

1 Answers1

3

Don't forget that the q you have access to in withObject is just a HashMap. So, you can write:

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 {..}

This will guard that the json only has at most the elements "foo" and "bar".

But, this does feel like overkill considering aeson gives you all of this for free. If you can derive Generic, then you can just call genericParseJSON, as in:

{-# 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
    }

Here we adjust the default parse options in two ways: first, we tell it to reject unknown fields, which is exactly what you're asking for, and second, we tell it how to get "foo" from the field name "mpFoo" (and likewise for bar).

DDub
  • 3,884
  • 1
  • 5
  • 12