Skip to content

Commit 14ca883

Browse files
committed
add reject_unknown_fields for DataclassValidator
1 parent 454035f commit 14ca883

3 files changed

Lines changed: 99 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
@@ -116,7 +116,12 @@ def __post_validate__(self, *, require_optional_field: bool = False):
116116
# Field default values
117117
field_defaults: dict[str, Default]
118118

119-
def __init__(self, dataclass_cls: type[T_Dataclass] | None = None) -> None:
119+
def __init__(
120+
self,
121+
dataclass_cls: type[T_Dataclass] | None = None,
122+
*,
123+
reject_unknown_fields: bool | None = None,
124+
) -> None:
120125
# For easier subclassing: If 'self.dataclass_cls' is already set (e.g. as class member in a subclass), use that
121126
# class as the default.
122127
if dataclass_cls is None:
@@ -133,7 +138,12 @@ def __init__(self, dataclass_cls: type[T_Dataclass] | None = None) -> None:
133138
raise InvalidValidatorOptionException('Parameter "dataclass_cls" must be a dataclass type.')
134139

135140
self.dataclass_cls = dataclass_cls
136-
self.reject_unknown_fields = getattr(dataclass_cls, '__reject_unknown_fields__', False)
141+
142+
# Use the explicit parameter if given, otherwise fall back to the dataclass setting
143+
if reject_unknown_fields is not None:
144+
self.reject_unknown_fields = reject_unknown_fields
145+
else:
146+
self.reject_unknown_fields = getattr(dataclass_cls, '__reject_unknown_fields__', False)
137147
self.field_defaults = {}
138148

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

tests/unit/validators/dataclass_validator_test.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1235,3 +1235,69 @@ class ExplicitAllowDataclass:
12351235
})
12361236

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

0 commit comments

Comments
 (0)