Skip to content

Commit 7362c1f

Browse files
committed
use DictValidator.default_validator with RejectValidator instead of own logic and exception in DataclassValidator
1 parent 81e5b42 commit 7362c1f

5 files changed

Lines changed: 34 additions & 95 deletions

File tree

src/validataclass/exceptions/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
InvalidTypeError,
1111
RequiredValueError,
1212
)
13-
from .dataclass_exceptions import UnknownFieldsError, DataclassPostValidationError
13+
from .dataclass_exceptions import DataclassPostValidationError
1414
from .datetime_exceptions import (
1515
DateTimeRangeError,
1616
InvalidDateError,
@@ -50,7 +50,6 @@
5050
from .url_exceptions import InvalidUrlError
5151

5252
__all__ = [
53-
'UnknownFieldsError',
5453
'DataclassInvalidPreValidateSignatureException',
5554
'DataclassPostValidationError',
5655
'DataclassValidatorFieldException',

src/validataclass/exceptions/dataclass_exceptions.py

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,33 +9,10 @@
99
from .base_exceptions import ValidationError
1010

1111
__all__ = [
12-
'UnknownFieldsError',
1312
'DataclassPostValidationError',
1413
]
1514

1615

17-
class UnknownFieldsError(ValidationError):
18-
"""
19-
Validation error raised by `DataclassValidator` when the input dictionary contains keys that do not correspond to
20-
any field in the dataclass, and `reject_unknown_fields` is set to `True`.
21-
22-
The `unknown_fields` attribute contains a sorted list of the extra field names.
23-
24-
Example as dictionary:
25-
26-
```
27-
{
28-
'code': 'unknown_fields',
29-
'unknown_fields': ['unknown1', 'unknown2'],
30-
}
31-
```
32-
"""
33-
code = 'unknown_fields'
34-
35-
def __init__(self, *, unknown_fields: list[str], **kwargs: Any):
36-
super().__init__(unknown_fields=sorted(unknown_fields), **kwargs)
37-
38-
3916
class DataclassPostValidationError(ValidationError):
4017
"""
4118
Validation error raised by `DataclassValidator` (or by a dataclass itself) when a user-defined post-validation

src/validataclass/validators/dataclass_validator.py

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@
1111

1212
from validataclass.dataclasses import Default, NoDefault
1313
from validataclass.exceptions import (
14-
UnknownFieldsError,
1514
DataclassInvalidPreValidateSignatureException,
1615
DataclassPostValidationError,
1716
DataclassValidatorFieldException,
1817
InvalidValidatorOptionException,
1918
ValidationError,
2019
)
2120
from .dict_validator import DictValidator
21+
from .reject_validator import RejectValidator
2222
from .validator import Validator
2323

2424
__all__ = [
@@ -110,9 +110,6 @@ def __post_validate__(self, *, require_optional_field: bool = False):
110110
# Dataclass type that the validated dictionary will be converted to
111111
dataclass_cls: type[T_Dataclass]
112112

113-
# Whether to reject unknown fields in the input dictionary
114-
reject_unknown_fields: bool
115-
116113
# Field default values
117114
field_defaults: dict[str, Default]
118115

@@ -140,10 +137,8 @@ def __init__(
140137
self.dataclass_cls = dataclass_cls
141138

142139
# 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)
140+
if reject_unknown_fields is None:
141+
reject_unknown_fields = getattr(dataclass_cls, '__reject_unknown_fields__', None)
147142
self.field_defaults = {}
148143

149144
# Collect field validators and required fields for the DictValidator by examining the dataclass fields
@@ -168,7 +163,12 @@ def __init__(
168163
required_fields.append(field.name)
169164

170165
# Initialize the underlying DictValidator
171-
super().__init__(field_validators=field_validators, required_fields=required_fields)
166+
default_validator = RejectValidator(error_reason='Unknown field') if reject_unknown_fields else None
167+
super().__init__(
168+
field_validators=field_validators,
169+
required_fields=required_fields,
170+
default_validator=default_validator,
171+
)
172172

173173
@staticmethod
174174
def _get_field_validator(field: dataclasses.Field[Any]) -> Validator:
@@ -238,13 +238,6 @@ def _pre_validate(self, input_data: Any, **kwargs: Any) -> dict[str, Any]:
238238
# Filter input dictionary through __pre_validate__()
239239
input_data = pre_validate_func(input_data, **context_kwargs)
240240

241-
# Check for unknown fields if not allowed
242-
if self.reject_unknown_fields:
243-
self._ensure_type(input_data, dict)
244-
unknown = sorted(set(input_data.keys()) - set(self.field_validators.keys()))
245-
if unknown:
246-
raise UnknownFieldsError(unknown_fields=unknown)
247-
248241
# Validate raw dictionary using underlying DictValidator
249242
validated_dict = super().validate(input_data, **kwargs)
250243

tests/unit/exceptions/dataclass_exceptions_test.py

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,52 +5,13 @@
55
"""
66

77
from validataclass.exceptions import (
8-
UnknownFieldsError,
98
DataclassPostValidationError,
109
DictRequiredFieldError,
1110
InvalidTypeError,
1211
ValidationError,
1312
)
1413

1514

16-
class UnknownFieldsErrorTest:
17-
"""
18-
Tests for the UnknownFieldsError exception class.
19-
"""
20-
21-
@staticmethod
22-
def test_unknown_fields_error_single_property():
23-
""" Tests UnknownFieldsError with a single additional property. """
24-
error = UnknownFieldsError(unknown_fields=['unknown_field'])
25-
26-
assert error.to_dict() == {
27-
'code': 'unknown_fields',
28-
'unknown_fields': ['unknown_field'],
29-
}
30-
31-
@staticmethod
32-
def test_unknown_fields_error_multiple_fields():
33-
""" Tests UnknownFieldsError with multiple additional fields (sorted). """
34-
error = UnknownFieldsError(unknown_fields=['watermelon', 'apple', 'mango'])
35-
36-
assert error.to_dict() == {
37-
'code': 'unknown_fields',
38-
'unknown_fields': ['apple', 'mango', 'watermelon'],
39-
}
40-
41-
@staticmethod
42-
def test_unknown_fields_error_repr():
43-
""" Tests repr of UnknownFieldsError. """
44-
error = UnknownFieldsError(unknown_fields=['unknown1', 'unknown2'])
45-
46-
assert (
47-
repr(error)
48-
== "UnknownFieldsError(code='unknown_fields', "
49-
"unknown_fields=['unknown1', 'unknown2'])"
50-
)
51-
assert str(error) == repr(error)
52-
53-
5415
class DataclassPostValidationErrorTest:
5516
"""
5617
Tests for the DataclassPostValidationError exception class.

tests/unit/validators/dataclass_validator_test.py

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
from tests.test_utils import UnitTestContextValidator
1414
from validataclass.dataclasses import Default, DefaultFactory, DefaultUnset, validataclass, validataclass_field
1515
from validataclass.exceptions import (
16-
UnknownFieldsError,
1716
DataclassInvalidPreValidateSignatureException,
1817
DataclassPostValidationError,
1918
DataclassValidatorFieldException,
@@ -1173,26 +1172,28 @@ def test_strict_dataclass_valid_with_optional_field_omitted():
11731172

11741173
@staticmethod
11751174
def test_strict_dataclass_with_additional_properties():
1176-
""" Test that a strict dataclass raises UnknownFieldsError for unknown keys. """
1175+
""" Test that a strict dataclass raises DictFieldsValidationError for unknown keys. """
11771176
validator = DataclassValidator(UnitTestStrictDataclass)
11781177

1179-
with pytest.raises(UnknownFieldsError) as exception_info:
1178+
with pytest.raises(DictFieldsValidationError) as exception_info:
11801179
validator.validate({
11811180
'name': 'banana',
11821181
'unknown_field': 'unknown_value',
11831182
})
11841183

11851184
assert exception_info.value.to_dict() == {
1186-
'code': 'unknown_fields',
1187-
'unknown_fields': ['unknown_field'],
1185+
'code': 'field_errors',
1186+
'field_errors': {
1187+
'unknown_field': {'code': 'field_not_allowed', 'reason': 'Unknown field'},
1188+
},
11881189
}
11891190

11901191
@staticmethod
11911192
def test_strict_dataclass_with_multiple_additional_fields():
1192-
""" Test that additional fields are sorted in the error. """
1193+
""" Test that each additional field gets its own error. """
11931194
validator = DataclassValidator(UnitTestStrictDataclass)
11941195

1195-
with pytest.raises(UnknownFieldsError) as exception_info:
1196+
with pytest.raises(DictFieldsValidationError) as exception_info:
11961197
validator.validate({
11971198
'name': 'banana',
11981199
'zebra': 1,
@@ -1201,8 +1202,12 @@ def test_strict_dataclass_with_multiple_additional_fields():
12011202
})
12021203

12031204
assert exception_info.value.to_dict() == {
1204-
'code': 'unknown_fields',
1205-
'unknown_fields': ['alpha', 'mango', 'zebra'],
1205+
'code': 'field_errors',
1206+
'field_errors': {
1207+
'alpha': {'code': 'field_not_allowed', 'reason': 'Unknown field'},
1208+
'mango': {'code': 'field_not_allowed', 'reason': 'Unknown field'},
1209+
'zebra': {'code': 'field_not_allowed', 'reason': 'Unknown field'},
1210+
},
12061211
}
12071212

12081213
@staticmethod
@@ -1243,7 +1248,7 @@ def test_reject_unknown_fields_validator_param_enables_rejection():
12431248
""" Test that reject_unknown_fields=True on DataclassValidator rejects unknown fields. """
12441249
validator = DataclassValidator(UnitTestDataclass, reject_unknown_fields=True)
12451250

1246-
with pytest.raises(UnknownFieldsError) as exception_info:
1251+
with pytest.raises(DictFieldsValidationError) as exception_info:
12471252
validator.validate({
12481253
'name': 'banana',
12491254
'color': 'yellow',
@@ -1253,8 +1258,10 @@ def test_reject_unknown_fields_validator_param_enables_rejection():
12531258
})
12541259

12551260
assert exception_info.value.to_dict() == {
1256-
'code': 'unknown_fields',
1257-
'unknown_fields': ['unknown_field'],
1261+
'code': 'field_errors',
1262+
'field_errors': {
1263+
'unknown_field': {'code': 'field_not_allowed', 'reason': 'Unknown field'},
1264+
},
12581265
}
12591266

12601267
@staticmethod
@@ -1294,7 +1301,7 @@ def test_reject_unknown_fields_validator_param_overrides_dataclass_false():
12941301
"""
12951302
validator = DataclassValidator(UnitTestDataclass, reject_unknown_fields=True)
12961303

1297-
with pytest.raises(UnknownFieldsError) as exception_info:
1304+
with pytest.raises(DictFieldsValidationError) as exception_info:
12981305
validator.validate({
12991306
'name': 'banana',
13001307
'color': 'yellow',
@@ -1304,6 +1311,8 @@ def test_reject_unknown_fields_validator_param_overrides_dataclass_false():
13041311
})
13051312

13061313
assert exception_info.value.to_dict() == {
1307-
'code': 'unknown_fields',
1308-
'unknown_fields': ['extra'],
1314+
'code': 'field_errors',
1315+
'field_errors': {
1316+
'extra': {'code': 'field_not_allowed', 'reason': 'Unknown field'},
1317+
},
13091318
}

0 commit comments

Comments
 (0)