Skip to content

Commit 94098eb

Browse files
committed
'rejectUnknownFields'
1 parent ad823dc commit 94098eb

6 files changed

Lines changed: 115 additions & 16 deletions

File tree

src/Data/OpenApi/Internal/Schema.hs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,6 +1041,7 @@ gproductSchema opts proxy = do
10411041
& minItems ?~ sz
10421042
_ -> mempty
10431043
& type_ ?~ OpenApiObject
1044+
& additionalProperties .~ noAdditionalProperties opts
10441045
& properties .~ foldl' (flip insProp) mempty recordFields
10451046
& required .~ requiredProps
10461047

@@ -1113,6 +1114,7 @@ gsumConToSchemaWith ref opts _ = case schema of
11131114
-- If it is not a record, we need to put subschema into "contents" field.
11141115
_ | not isRecord -> Inline $ mempty
11151116
& type_ ?~ OpenApiObject
1117+
& additionalProperties .~ noAdditionalProperties opts
11161118
& required .~ [T.pack tagField]
11171119
& properties . at (T.pack tagField) ?~ tagString
11181120
-- If constructor is nullary, there is no content.
@@ -1124,6 +1126,7 @@ gsumConToSchemaWith ref opts _ = case schema of
11241126
_ -> Inline $ mempty
11251127
& allOf ?~ [Inline $ mempty
11261128
& type_ ?~ OpenApiObject
1129+
& additionalProperties .~ noAdditionalProperties opts
11271130
& required .~ (T.pack tagField : if isRecord then [] else [T.pack contentsField])
11281131
& properties . at (T.pack tagField) ?~ tagString]
11291132
& if isRecord
@@ -1132,6 +1135,8 @@ gsumConToSchemaWith ref opts _ = case schema of
11321135
UntaggedValue -> refOrEnum -- Aeson encodes nullary constructors as strings in this case.
11331136
ObjectWithSingleField -> Inline $ mempty
11341137
& type_ ?~ OpenApiObject
1138+
-- This is how "aeson" behaves
1139+
& additionalProperties ?~ AdditionalPropertiesAllowed False
11351140
& required .~ [tag]
11361141
& properties . at tag ?~ refOrNullary
11371142
TwoElemArray -> Inline $ mempty
@@ -1192,6 +1197,13 @@ instance {-# OVERLAPPABLE #-} AllNullaryConstructors (C1 c f) where
11921197
instance {-# OVERLAPPING #-} (Constructor c) => AllNullaryConstructors (C1 c U1) where
11931198
nullaryConstructorsNames _ = Just [conName $ Proxy3 @c @_ @_]
11941199

1200+
noAdditionalProperties :: SchemaOptions -> Maybe AdditionalProperties
1201+
noAdditionalProperties opts = do
1202+
-- Missing 'additionalProperties' serve as set to 'true',
1203+
-- so we set this only when 'rejectUnknownFields' is 'false'.
1204+
guard $ rejectUnknownFields opts
1205+
Just $ AdditionalPropertiesAllowed False
1206+
11951207
{- $setup
11961208
>>> import Data.OpenApi
11971209
>>> import Data.Aeson (encode)

src/Data/OpenApi/Internal/Schema/Validation.hs

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -412,21 +412,14 @@ validateObject o = withSchema $ \sch ->
412412

413413
validateProps = withSchema $ \sch -> do
414414
for_ (objectToList o) $ \(keyToText -> k, v) ->
415-
case v of
416-
Null | not (k `elem` (sch ^. required)) -> valid -- null is fine for non-required property
417-
_ ->
418-
case InsOrdHashMap.lookup k (sch ^. properties) of
419-
Nothing -> checkMissing (unknownProperty k) additionalProperties $ validateAdditional k v
420-
Just s -> validateWithSchemaRef s v
415+
case InsOrdHashMap.lookup k (sch ^. properties) of
416+
Nothing -> checkMissing valid additionalProperties $ validateAdditional k v
417+
Just s -> validateWithSchemaRef s v
421418

422419
validateAdditional _ _ (AdditionalPropertiesAllowed True) = valid
423420
validateAdditional k _ (AdditionalPropertiesAllowed False) = invalid $ "additionalProperties=false but extra property " <> show k <> " found"
424421
validateAdditional _ v (AdditionalPropertiesSchema s) = validateWithSchemaRef s v
425422

426-
unknownProperty :: Text -> Validation s a
427-
unknownProperty pname = invalid $
428-
"property " <> show pname <> " is found in JSON value, but it is not mentioned in Swagger schema"
429-
430423
validateEnum :: Value -> Validation Schema ()
431424
validateEnum val = do
432425
check enum_ $ \xs ->

src/Data/OpenApi/SchemaOptions.hs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ data SchemaOptions = SchemaOptions
2727
-- | Encode types with a single constructor as sums,
2828
-- so that `allNullaryToStringTag` and `sumEncoding` apply.
2929
, tagSingleConstructors :: Bool
30+
, rejectUnknownFields :: Bool
3031
}
3132

3233
-- | Default encoding @'SchemaOptions'@.
@@ -54,11 +55,12 @@ defaultSchemaOptions = fromAesonOptions Aeson.defaultOptions
5455
-- * 'unwrapUnaryRecords'
5556
-- * 'sumEncoding'
5657
-- * 'tagSingleConstructors'
58+
-- * 'rejectUnknownFields'
5759
--
5860
-- Note that these fields have no effect on `SchemaOptions`:
5961
--
6062
-- * 'Aeson.omitNothingFields'
61-
-- * 'Aeson.rejectUnknownFields'
63+
-- * 'Aeson.allowOmittedFields' (introduced in @aeson-2.2@)
6264
--
6365
-- The rest is defined as in 'defaultSchemaOptions'.
6466
--
@@ -73,4 +75,5 @@ fromAesonOptions opts = SchemaOptions
7375
, unwrapUnaryRecords = Aeson.unwrapUnaryRecords opts
7476
, sumEncoding = Aeson.sumEncoding opts
7577
, tagSingleConstructors = Aeson.tagSingleConstructors opts
78+
, rejectUnknownFields = Aeson.rejectUnknownFields opts
7679
}

test/Data/OpenApi/CommonTestTypes.hs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{-# LANGUAGE DeriveGeneric #-}
2+
{-# LANGUAGE DuplicateRecordFields #-}
23
{-# LANGUAGE QuasiQuotes #-}
34
{-# LANGUAGE ScopedTypeVariables #-}
45

@@ -1102,3 +1103,68 @@ unsignedIntsSchemaJSON = [aesonQQ|
11021103
"required": ["uint32", "uint64"]
11031104
}
11041105
|]
1106+
1107+
-- ========================================================================
1108+
-- AdditionalProperties
1109+
-- ========================================================================
1110+
1111+
data AdditionalPropertiesYes = AdditionalPropertiesYes
1112+
{ prop1 :: Bool
1113+
, prop2 :: Int
1114+
} deriving (Generic)
1115+
instance ToSchema AdditionalPropertiesYes
1116+
1117+
additionalPropYesSchema :: Value
1118+
additionalPropYesSchema = [aesonQQ|
1119+
{
1120+
"type": "object",
1121+
"properties": {
1122+
"prop1": {
1123+
"type": "boolean"
1124+
},
1125+
"prop2": {
1126+
"maximum": 9223372036854775807,
1127+
"minimum": -9223372036854775808,
1128+
"type": "integer"
1129+
}
1130+
},
1131+
"required": [
1132+
"prop1",
1133+
"prop2"
1134+
]
1135+
}
1136+
|]
1137+
1138+
data AdditionalPropertiesNo = AdditionalPropertiesNo
1139+
{ prop1 :: Bool
1140+
, prop2 :: Int
1141+
} deriving (Generic)
1142+
instance ToSchema AdditionalPropertiesNo where
1143+
declareNamedSchema = genericDeclareNamedSchema
1144+
defaultSchemaOptions{Data.OpenApi.rejectUnknownFields = True}
1145+
1146+
additionalPropNoSchema :: Value
1147+
additionalPropNoSchema = [aesonQQ|
1148+
{
1149+
"type": "object",
1150+
"properties": {
1151+
"prop1": {
1152+
"type": "boolean"
1153+
},
1154+
"prop2": {
1155+
"maximum": 9223372036854775807,
1156+
"minimum": -9223372036854775808,
1157+
"type": "integer"
1158+
}
1159+
},
1160+
"required": [
1161+
"prop1",
1162+
"prop2"
1163+
],
1164+
"additionalProperties": false
1165+
}
1166+
|]
1167+
1168+
--------------------------
1169+
--------------------------
1170+
--------------------------

test/Data/OpenApi/Schema/ValidationSpec.hs

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
{-# LANGUAGE QuasiQuotes #-}
55
{-# LANGUAGE RecordWildCards #-}
66
{-# LANGUAGE ScopedTypeVariables #-}
7+
{-# LANGUAGE TypeApplications #-}
78
{-# OPTIONS_GHC -fno-warn-orphans #-}
89
module Data.OpenApi.Schema.ValidationSpec where
910

@@ -40,6 +41,8 @@ import Data.OpenApi
4041
import Data.OpenApi.Declare
4142
import Data.OpenApi.Aeson.Compat (stringToKey)
4243

44+
import Data.OpenApi.CommonTestTypes
45+
(AdditionalPropertiesYes, AdditionalPropertiesNo)
4346
import Test.Hspec
4447
import Test.Hspec.QuickCheck
4548
import Test.QuickCheck
@@ -49,11 +52,17 @@ shouldValidate :: (ToJSON a, ToSchema a) => Proxy a -> a -> Bool
4952
shouldValidate _ x = validateToJSON x == []
5053

5154
shouldValidateValue :: (ToSchema a) => Proxy a -> Value -> Expectation
52-
shouldValidateValue px val = do
55+
shouldValidateValue px val = case validateValue px val of
56+
[] -> pure ()
57+
errors -> expectationFailure $ unlines errors
58+
59+
shouldNotValidateValue :: (ToSchema a) => Proxy a -> Value -> [String] -> Expectation
60+
shouldNotValidateValue px val = shouldMatchList (validateValue px val)
61+
62+
validateValue :: (ToSchema a) => Proxy a -> Value -> [String]
63+
validateValue px val =
5364
let (defs, sch) = runDeclare (declareSchema px) mempty
54-
case validateJSON defs sch val of
55-
[] -> pure ()
56-
errors -> expectationFailure $ unlines errors
65+
in validateJSON defs sch val
5766

5867
shouldNotValidate :: forall a. ToSchema a => (a -> Value) -> a -> Bool
5968
shouldNotValidate f = not . null . validateJSON defs sch . f
@@ -123,7 +132,19 @@ spec = do
123132
prop "invalidColorToJSON" $ shouldNotValidate invalidColorToJSON
124133
prop "invalidPaintToJSON" $ shouldNotValidate invalidPaintToJSON
125134
prop "invalidLightToJSON" $ shouldNotValidate invalidLightToJSON
126-
prop "invalidButtonImagesToJSON" $ shouldNotValidate invalidButtonImagesToJSON
135+
describe "rejectUnknownFields" $ do
136+
let val = [aesonQQ|
137+
{
138+
"prop1" : true,
139+
"prop2" : 1,
140+
"prop3" : null
141+
}
142+
|]
143+
it "disabled" $
144+
shouldValidateValue (Proxy @AdditionalPropertiesYes) val
145+
it "enabled" $
146+
shouldNotValidateValue (Proxy @AdditionalPropertiesNo) val
147+
["additionalProperties=false but extra property \"prop3\" found"]
127148

128149
main :: IO ()
129150
main = hspec spec

test/Data/OpenApi/SchemaSpec.hs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{-# LANGUAGE DeriveGeneric #-}
22
{-# LANGUAGE ScopedTypeVariables #-}
33
{-# LANGUAGE TemplateHaskell #-}
4+
{-# LANGUAGE TypeApplications #-}
45
{-# LANGUAGE QuasiQuotes #-}
56
module Data.OpenApi.SchemaSpec where
67

@@ -115,6 +116,9 @@ spec = do
115116
describe "Bounded Enum key mapping" $ do
116117
context "ButtonImages" $ checkToSchema (Proxy :: Proxy ButtonImages) buttonImagesSchemaJSON
117118
context "TimeOfDay" $ checkToSchema (Proxy :: Proxy Data.Time.LocalTime.TimeOfDay) timeOfDaySchemaJSON
119+
describe "rejectUnknownFields" $ do
120+
describe "disabled" $ checkToSchema (Proxy @AdditionalPropertiesYes) additionalPropYesSchema
121+
describe "enabled" $ checkToSchema (Proxy @AdditionalPropertiesNo) additionalPropNoSchema
118122

119123
main :: IO ()
120124
main = hspec spec

0 commit comments

Comments
 (0)