Skip to content

Commit eee02a4

Browse files
committed
Added ListSerializer.validate(). Closes #2168.
1 parent ef89c15 commit eee02a4

File tree

3 files changed

+108
-54
lines changed

3 files changed

+108
-54
lines changed

rest_framework/fields.py

+27-11
Original file line numberDiff line numberDiff line change
@@ -294,31 +294,47 @@ def get_default(self):
294294
return self.default()
295295
return self.default
296296

297-
def run_validation(self, data=empty):
297+
def validate_empty_values(self, data):
298298
"""
299-
Validate a simple representation and return the internal value.
300-
301-
The provided data may be `empty` if no representation was included
302-
in the input.
303-
304-
May raise `SkipField` if the field should not be included in the
305-
validated data.
299+
Validate empty values, and either:
300+
301+
* Raise `ValidationError`, indicating invalid data.
302+
* Raise `SkipField`, indicating that the field should be ignored.
303+
* Return (True, data), indicating an empty value that should be
304+
returned without any furhter validation being applied.
305+
* Return (False, data), indicating a non-empty value, that should
306+
have validation applied as normal.
306307
"""
307308
if self.read_only:
308-
return self.get_default()
309+
return (True, self.get_default())
309310

310311
if data is empty:
311312
if getattr(self.root, 'partial', False):
312313
raise SkipField()
313314
if self.required:
314315
self.fail('required')
315-
return self.get_default()
316+
return (True, self.get_default())
316317

317318
if data is None:
318319
if not self.allow_null:
319320
self.fail('null')
320-
return None
321+
return (True, None)
322+
323+
return (False, data)
321324

325+
def run_validation(self, data=empty):
326+
"""
327+
Validate a simple representation and return the internal value.
328+
329+
The provided data may be `empty` if no representation was included
330+
in the input.
331+
332+
May raise `SkipField` if the field should not be included in the
333+
validated data.
334+
"""
335+
(is_empty_value, data) = self.validate_empty_values(data)
336+
if is_empty_value:
337+
return data
322338
value = self.to_internal_value(data)
323339
self.run_validators(value)
324340
return value

rest_framework/serializers.py

+65-43
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,35 @@ def __new__(cls, name, bases, attrs):
229229
return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs)
230230

231231

232+
def get_validation_error_detail(exc):
233+
assert isinstance(exc, (ValidationError, DjangoValidationError))
234+
235+
if isinstance(exc, DjangoValidationError):
236+
# Normally you should raise `serializers.ValidationError`
237+
# inside your codebase, but we handle Django's validation
238+
# exception class as well for simpler compat.
239+
# Eg. Calling Model.clean() explicitly inside Serializer.validate()
240+
return {
241+
api_settings.NON_FIELD_ERRORS_KEY: list(exc.messages)
242+
}
243+
elif isinstance(exc.detail, dict):
244+
# If errors may be a dict we use the standard {key: list of values}.
245+
# Here we ensure that all the values are *lists* of errors.
246+
return dict([
247+
(key, value if isinstance(value, list) else [value])
248+
for key, value in exc.detail.items()
249+
])
250+
elif isinstance(exc.detail, list):
251+
# Errors raised as a list are non-field errors.
252+
return {
253+
api_settings.NON_FIELD_ERRORS_KEY: exc.detail
254+
}
255+
# Errors raised as a string are non-field errors.
256+
return {
257+
api_settings.NON_FIELD_ERRORS_KEY: [exc.detail]
258+
}
259+
260+
232261
@six.add_metaclass(SerializerMetaclass)
233262
class Serializer(BaseSerializer):
234263
default_error_messages = {
@@ -293,62 +322,32 @@ def run_validation(self, data=empty):
293322
performed by validators and the `.validate()` method should
294323
be coerced into an error dictionary with a 'non_fields_error' key.
295324
"""
296-
if data is empty:
297-
if getattr(self.root, 'partial', False):
298-
raise SkipField()
299-
if self.required:
300-
self.fail('required')
301-
return self.get_default()
302-
303-
if data is None:
304-
if not self.allow_null:
305-
self.fail('null')
306-
return None
307-
308-
if not isinstance(data, dict):
309-
message = self.error_messages['invalid'].format(
310-
datatype=type(data).__name__
311-
)
312-
raise ValidationError({
313-
api_settings.NON_FIELD_ERRORS_KEY: [message]
314-
})
325+
(is_empty_value, data) = self.validate_empty_values(data)
326+
if is_empty_value:
327+
return data
315328

316329
value = self.to_internal_value(data)
317330
try:
318331
self.run_validators(value)
319332
value = self.validate(value)
320333
assert value is not None, '.validate() should return the validated data'
321-
except ValidationError as exc:
322-
if isinstance(exc.detail, dict):
323-
# .validate() errors may be a dict, in which case, use
324-
# standard {key: list of values} style.
325-
raise ValidationError(dict([
326-
(key, value if isinstance(value, list) else [value])
327-
for key, value in exc.detail.items()
328-
]))
329-
elif isinstance(exc.detail, list):
330-
raise ValidationError({
331-
api_settings.NON_FIELD_ERRORS_KEY: exc.detail
332-
})
333-
else:
334-
raise ValidationError({
335-
api_settings.NON_FIELD_ERRORS_KEY: [exc.detail]
336-
})
337-
except DjangoValidationError as exc:
338-
# Normally you should raise `serializers.ValidationError`
339-
# inside your codebase, but we handle Django's validation
340-
# exception class as well for simpler compat.
341-
# Eg. Calling Model.clean() explicitly inside Serializer.validate()
342-
raise ValidationError({
343-
api_settings.NON_FIELD_ERRORS_KEY: list(exc.messages)
344-
})
334+
except (ValidationError, DjangoValidationError) as exc:
335+
raise ValidationError(detail=get_validation_error_detail(exc))
345336

346337
return value
347338

348339
def to_internal_value(self, data):
349340
"""
350341
Dict of native values <- Dict of primitive datatypes.
351342
"""
343+
if not isinstance(data, dict):
344+
message = self.error_messages['invalid'].format(
345+
datatype=type(data).__name__
346+
)
347+
raise ValidationError({
348+
api_settings.NON_FIELD_ERRORS_KEY: [message]
349+
})
350+
352351
ret = OrderedDict()
353352
errors = OrderedDict()
354353
fields = [
@@ -462,6 +461,26 @@ def get_value(self, dictionary):
462461
return html.parse_html_list(dictionary, prefix=self.field_name)
463462
return dictionary.get(self.field_name, empty)
464463

464+
def run_validation(self, data=empty):
465+
"""
466+
We override the default `run_validation`, because the validation
467+
performed by validators and the `.validate()` method should
468+
be coerced into an error dictionary with a 'non_fields_error' key.
469+
"""
470+
(is_empty_value, data) = self.validate_empty_values(data)
471+
if is_empty_value:
472+
return data
473+
474+
value = self.to_internal_value(data)
475+
try:
476+
self.run_validators(value)
477+
value = self.validate(value)
478+
assert value is not None, '.validate() should return the validated data'
479+
except (ValidationError, DjangoValidationError) as exc:
480+
raise ValidationError(detail=get_validation_error_detail(exc))
481+
482+
return value
483+
465484
def to_internal_value(self, data):
466485
"""
467486
List of dicts of native values <- List of dicts of primitive datatypes.
@@ -503,6 +522,9 @@ def to_representation(self, data):
503522
self.child.to_representation(item) for item in iterable
504523
]
505524

525+
def validate(self, attrs):
526+
return attrs
527+
506528
def update(self, instance, validated_data):
507529
raise NotImplementedError(
508530
"Serializers with many=True do not support multiple update by "

tests/test_serializer_lists.py

+16
Original file line numberDiff line numberDiff line change
@@ -272,3 +272,19 @@ def test_validate_html_input(self):
272272
serializer = self.Serializer(data=input_data)
273273
assert serializer.is_valid()
274274
assert serializer.validated_data == expected_output
275+
276+
277+
class TestListSerializerClass:
278+
"""Tests for a custom list_serializer_class."""
279+
def test_list_serializer_class_validate(self):
280+
class CustomListSerializer(serializers.ListSerializer):
281+
def validate(self, attrs):
282+
raise serializers.ValidationError('Non field error')
283+
284+
class TestSerializer(serializers.Serializer):
285+
class Meta:
286+
list_serializer_class = CustomListSerializer
287+
288+
serializer = TestSerializer(data=[], many=True)
289+
assert not serializer.is_valid()
290+
assert serializer.errors == {'non_field_errors': ['Non field error']}

0 commit comments

Comments
 (0)