Skip to content

Commit 57ca52a

Browse files
committed
add reject_unknown_fields for DataclassValidator
1 parent 71d4d72 commit 57ca52a

3 files changed

Lines changed: 105 additions & 4 deletions

File tree

docs/05-dataclasses.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,8 +304,10 @@ This makes sense for normal APIs, as additional fields are just filtered out, an
304304
changes in the API. There might be situations where one needs to have a strict validation of additional fields,
305305
for example, to match an OpenAPI validation.
306306

307-
You can reject unknown fields by setting `reject_unknown_fields` at the `@validataclass` to `True`,
308-
like this:
307+
You can reject unknown fields by setting `reject_unknown_fields` either on the `@validataclass` decorator or on the
308+
`DataclassValidator` itself. When set on the `DataclassValidator`, it overrides the setting from the decorator.
309+
310+
Setting it on the decorator:
309311

310312
```python
311313
from validataclass.dataclasses import validataclass
@@ -321,6 +323,23 @@ my_validator.validate({'name': 'test'}) # would work fine
321323
my_validator.validate({'name': 'test', 'more': 'stuff'}) # would throw an UnknownFieldsError
322324
```
323325

326+
Setting it on the `DataclassValidator` (this also works with dataclasses that don't have `reject_unknown_fields` set
327+
on the decorator, and can override the decorator setting):
328+
329+
```python
330+
from validataclass.dataclasses import validataclass
331+
from validataclass.validators import DataclassValidator, StringValidator
332+
333+
@validataclass
334+
class MyModel:
335+
name: str = StringValidator()
336+
337+
my_validator = DataclassValidator(MyModel, reject_unknown_fields=True)
338+
339+
my_validator.validate({'name': 'test'}) # would work fine
340+
my_validator.validate({'name': 'test', 'more': 'stuff'}) # would throw an UnknownFieldsError
341+
```
342+
324343

325344
## Defining field defaults
326345

src/validataclass/validators/dataclass_validator.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,12 @@ def __post_validate__(self, *, require_optional_field: bool = False):
121121
# Field default values
122122
field_defaults: dict[str, BaseDefault[Any]]
123123

124-
def __init__(self, dataclass_cls: type[T_Dataclass] | None = None) -> None:
124+
def __init__(
125+
self,
126+
dataclass_cls: type[T_Dataclass] | None = None,
127+
*,
128+
reject_unknown_fields: bool | None = None,
129+
) -> None:
125130
# For easier subclassing: If 'self.dataclass_cls' is already set (e.g. as class member in a subclass), use that
126131
# class as the default.
127132
if dataclass_cls is None:
@@ -138,7 +143,12 @@ def __init__(self, dataclass_cls: type[T_Dataclass] | None = None) -> None:
138143
raise InvalidValidatorOptionException('Parameter "dataclass_cls" must be a dataclass type.')
139144

140145
self.dataclass_cls = dataclass_cls
141-
self.reject_unknown_fields = getattr(dataclass_cls, '__reject_unknown_fields__', False)
146+
147+
# Use the explicit parameter if given, otherwise fall back to the dataclass setting
148+
if reject_unknown_fields is not None:
149+
self.reject_unknown_fields = reject_unknown_fields
150+
else:
151+
self.reject_unknown_fields = getattr(dataclass_cls, '__reject_unknown_fields__', False)
142152
self.field_defaults = {}
143153

144154
# Collect field validators and required fields for the DictValidator by examining the dataclass fields

tests/unit/validators/dataclass_validator_test.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1242,3 +1242,75 @@ class ExplicitAllowDataclass:
12421242
})
12431243

12441244
assert validated_data.name == 'banana'
1245+
1246+
# Tests for reject_unknown_fields as DataclassValidator init parameter
1247+
1248+
@staticmethod
1249+
def test_reject_unknown_fields_validator_param_enables_rejection():
1250+
""" Test that reject_unknown_fields=True on DataclassValidator rejects unknown fields. """
1251+
validator = DataclassValidator(UnitTestDataclass, reject_unknown_fields=True)
1252+
1253+
with pytest.raises(UnknownFieldsError) as exception_info:
1254+
validator.validate({
1255+
'name': 'banana',
1256+
'color': 'yellow',
1257+
'amount': 10,
1258+
'weight': '1.234',
1259+
'unknown_field': 'unknown_value',
1260+
})
1261+
1262+
assert exception_info.value.to_dict() == {
1263+
'code': 'unknown_fields',
1264+
'unknown_fields': ['unknown_field'],
1265+
}
1266+
1267+
@staticmethod
1268+
def test_reject_unknown_fields_validator_param_allows_valid_input():
1269+
""" Test that reject_unknown_fields=True on DataclassValidator allows valid input without unknown fields. """
1270+
validator = DataclassValidator(UnitTestDataclass, reject_unknown_fields=True)
1271+
validated_data = validator.validate({
1272+
'name': 'banana',
1273+
'color': 'yellow',
1274+
'amount': 10,
1275+
'weight': '1.234',
1276+
})
1277+
1278+
assert type(validated_data) is UnitTestDataclass
1279+
assert validated_data.name == 'banana'
1280+
1281+
@staticmethod
1282+
def test_reject_unknown_fields_validator_param_overrides_dataclass_true():
1283+
"""
1284+
Test that reject_unknown_fields=False on DataclassValidator overrides reject_unknown_fields=True on the
1285+
dataclass.
1286+
"""
1287+
validator = DataclassValidator(UnitTestStrictDataclass, reject_unknown_fields=False)
1288+
validated_data = validator.validate({
1289+
'name': 'banana',
1290+
'extra': 'ignored',
1291+
})
1292+
1293+
assert type(validated_data) is UnitTestStrictDataclass
1294+
assert validated_data.name == 'banana'
1295+
1296+
@staticmethod
1297+
def test_reject_unknown_fields_validator_param_overrides_dataclass_false():
1298+
"""
1299+
Test that reject_unknown_fields=True on DataclassValidator overrides reject_unknown_fields=False on the
1300+
dataclass.
1301+
"""
1302+
validator = DataclassValidator(UnitTestDataclass, reject_unknown_fields=True)
1303+
1304+
with pytest.raises(UnknownFieldsError) as exception_info:
1305+
validator.validate({
1306+
'name': 'banana',
1307+
'color': 'yellow',
1308+
'amount': 10,
1309+
'weight': '1.234',
1310+
'extra': 'not allowed',
1311+
})
1312+
1313+
assert exception_info.value.to_dict() == {
1314+
'code': 'unknown_fields',
1315+
'unknown_fields': ['extra'],
1316+
}

0 commit comments

Comments
 (0)