Skip to content

Commit 1c3a40f

Browse files
committed
use DictValidator.default_validator with RejectValidator instead of own logic and exception in DataclassValidator
1 parent 57ca52a commit 1c3a40f

5 files changed

Lines changed: 35 additions & 96 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
@@ -11,33 +11,10 @@
1111
from .base_exceptions import ValidationError
1212

1313
__all__ = [
14-
'UnknownFieldsError',
1514
'DataclassPostValidationError',
1615
]
1716

1817

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

src/validataclass/validators/dataclass_validator.py

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@
1313

1414
from validataclass.dataclasses import BaseDefault, NoDefault
1515
from validataclass.exceptions import (
16-
UnknownFieldsError,
1716
DataclassInvalidPreValidateSignatureException,
1817
DataclassPostValidationError,
1918
DataclassValidatorFieldException,
2019
InvalidValidatorOptionException,
2120
ValidationError,
2221
)
2322
from .dict_validator import DictValidator
23+
from .reject_validator import RejectValidator
2424
from .validator import Validator
2525

2626
__all__ = [
@@ -115,9 +115,6 @@ def __post_validate__(self, *, require_optional_field: bool = False):
115115
# Dataclass type that the validated dictionary will be converted to
116116
dataclass_cls: type[T_Dataclass]
117117

118-
# Whether to reject unknown fields in the input dictionary
119-
reject_unknown_fields: bool
120-
121118
# Field default values
122119
field_defaults: dict[str, BaseDefault[Any]]
123120

@@ -145,10 +142,8 @@ def __init__(
145142
self.dataclass_cls = dataclass_cls
146143

147144
# 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)
145+
if reject_unknown_fields is None:
146+
reject_unknown_fields = getattr(dataclass_cls, '__reject_unknown_fields__', None)
152147
self.field_defaults = {}
153148

154149
# Collect field validators and required fields for the DictValidator by examining the dataclass fields
@@ -173,8 +168,13 @@ def __init__(
173168
required_fields.append(field.name)
174169

175170
# Initialize the DictValidator
176-
self.dict_validator = DictValidator(field_validators=field_validators, required_fields=required_fields)
177-
171+
default_validator = RejectValidator(error_reason='Unknown field') if reject_unknown_fields else None
172+
self.dict_validator = DictValidator(
173+
field_validators=field_validators,
174+
required_fields=required_fields,
175+
default_validator=default_validator,
176+
)
177+
178178
@staticmethod
179179
def _get_field_validator(field: dataclasses.Field[Any]) -> Validator[Any]:
180180
# Parse field metadata to get Validator
@@ -243,13 +243,6 @@ def _pre_validate(self, input_data: Any, **kwargs: Any) -> dict[str, Any]:
243243
# Filter input dictionary through __pre_validate__()
244244
input_data = pre_validate_func(input_data, **context_kwargs)
245245

246-
# Check for unknown fields if not allowed
247-
if self.reject_unknown_fields:
248-
self._ensure_type(input_data, dict)
249-
unknown = sorted(set(input_data.keys()) - set(self.field_validators.keys()))
250-
if unknown:
251-
raise UnknownFieldsError(unknown_fields=unknown)
252-
253246
# Validate raw dictionary using underlying DictValidator
254247
validated_dict = self.dict_validator.validate(input_data, **kwargs)
255248

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
@@ -14,7 +14,6 @@
1414
from tests.test_utils import UnitTestContextValidator
1515
from validataclass.dataclasses import Default, DefaultFactory, DefaultUnset, validataclass, validataclass_field
1616
from validataclass.exceptions import (
17-
UnknownFieldsError,
1817
DataclassInvalidPreValidateSignatureException,
1918
DataclassPostValidationError,
2019
DataclassValidatorFieldException,
@@ -1180,26 +1179,28 @@ def test_strict_dataclass_valid_with_optional_field_omitted():
11801179

11811180
@staticmethod
11821181
def test_strict_dataclass_with_additional_properties():
1183-
""" Test that a strict dataclass raises UnknownFieldsError for unknown keys. """
1182+
""" Test that a strict dataclass raises DictFieldsValidationError for unknown keys. """
11841183
validator = DataclassValidator(UnitTestStrictDataclass)
11851184

1186-
with pytest.raises(UnknownFieldsError) as exception_info:
1185+
with pytest.raises(DictFieldsValidationError) as exception_info:
11871186
validator.validate({
11881187
'name': 'banana',
11891188
'unknown_field': 'unknown_value',
11901189
})
11911190

11921191
assert exception_info.value.to_dict() == {
1193-
'code': 'unknown_fields',
1194-
'unknown_fields': ['unknown_field'],
1192+
'code': 'field_errors',
1193+
'field_errors': {
1194+
'unknown_field': {'code': 'field_not_allowed', 'reason': 'Unknown field'},
1195+
},
11951196
}
11961197

11971198
@staticmethod
11981199
def test_strict_dataclass_with_multiple_additional_fields():
1199-
""" Test that additional fields are sorted in the error. """
1200+
""" Test that each additional field gets its own error. """
12001201
validator = DataclassValidator(UnitTestStrictDataclass)
12011202

1202-
with pytest.raises(UnknownFieldsError) as exception_info:
1203+
with pytest.raises(DictFieldsValidationError) as exception_info:
12031204
validator.validate({
12041205
'name': 'banana',
12051206
'zebra': 1,
@@ -1208,8 +1209,12 @@ def test_strict_dataclass_with_multiple_additional_fields():
12081209
})
12091210

12101211
assert exception_info.value.to_dict() == {
1211-
'code': 'unknown_fields',
1212-
'unknown_fields': ['alpha', 'mango', 'zebra'],
1212+
'code': 'field_errors',
1213+
'field_errors': {
1214+
'alpha': {'code': 'field_not_allowed', 'reason': 'Unknown field'},
1215+
'mango': {'code': 'field_not_allowed', 'reason': 'Unknown field'},
1216+
'zebra': {'code': 'field_not_allowed', 'reason': 'Unknown field'},
1217+
},
12131218
}
12141219

12151220
@staticmethod
@@ -1250,7 +1255,7 @@ def test_reject_unknown_fields_validator_param_enables_rejection():
12501255
""" Test that reject_unknown_fields=True on DataclassValidator rejects unknown fields. """
12511256
validator = DataclassValidator(UnitTestDataclass, reject_unknown_fields=True)
12521257

1253-
with pytest.raises(UnknownFieldsError) as exception_info:
1258+
with pytest.raises(DictFieldsValidationError) as exception_info:
12541259
validator.validate({
12551260
'name': 'banana',
12561261
'color': 'yellow',
@@ -1260,8 +1265,10 @@ def test_reject_unknown_fields_validator_param_enables_rejection():
12601265
})
12611266

12621267
assert exception_info.value.to_dict() == {
1263-
'code': 'unknown_fields',
1264-
'unknown_fields': ['unknown_field'],
1268+
'code': 'field_errors',
1269+
'field_errors': {
1270+
'unknown_field': {'code': 'field_not_allowed', 'reason': 'Unknown field'},
1271+
},
12651272
}
12661273

12671274
@staticmethod
@@ -1301,7 +1308,7 @@ def test_reject_unknown_fields_validator_param_overrides_dataclass_false():
13011308
"""
13021309
validator = DataclassValidator(UnitTestDataclass, reject_unknown_fields=True)
13031310

1304-
with pytest.raises(UnknownFieldsError) as exception_info:
1311+
with pytest.raises(DictFieldsValidationError) as exception_info:
13051312
validator.validate({
13061313
'name': 'banana',
13071314
'color': 'yellow',
@@ -1311,6 +1318,8 @@ def test_reject_unknown_fields_validator_param_overrides_dataclass_false():
13111318
})
13121319

13131320
assert exception_info.value.to_dict() == {
1314-
'code': 'unknown_fields',
1315-
'unknown_fields': ['extra'],
1321+
'code': 'field_errors',
1322+
'field_errors': {
1323+
'extra': {'code': 'field_not_allowed', 'reason': 'Unknown field'},
1324+
},
13161325
}

0 commit comments

Comments
 (0)