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
?
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
).