Repository: encode/django-rest-framework

Issue:
Add `ListSerializer.validate()`
In d2d7e1d `Field.validate` was removed. This means that `ListSerializer.validate` is no longer called in `ListSerializer.run_validation`. I can use `ListSerializer.to_internal_value` instead, but this feels very weird on a serializer.



Discussion:
> Should ListSerializer.validate exist?

Yes, I think it should.


Resolving patch:
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -294,31 +294,47 @@ def get_default(self):
             return self.default()
         return self.default
 
-    def run_validation(self, data=empty):
+    def validate_empty_values(self, data):
         """
-        Validate a simple representation and return the internal value.
-
-        The provided data may be `empty` if no representation was included
-        in the input.
-
-        May raise `SkipField` if the field should not be included in the
-        validated data.
+        Validate empty values, and either:
+
+        * Raise `ValidationError`, indicating invalid data.
+        * Raise `SkipField`, indicating that the field should be ignored.
+        * Return (True, data), indicating an empty value that should be
+          returned without any furhter validation being applied.
+        * Return (False, data), indicating a non-empty value, that should
+          have validation applied as normal.
         """
         if self.read_only:
-            return self.get_default()
+            return (True, self.get_default())
 
         if data is empty:
             if getattr(self.root, 'partial', False):
                 raise SkipField()
             if self.required:
                 self.fail('required')
-            return self.get_default()
+            return (True, self.get_default())
 
         if data is None:
             if not self.allow_null:
                 self.fail('null')
-            return None
+            return (True, None)
+
+        return (False, data)
 
+    def run_validation(self, data=empty):
+        """
+        Validate a simple representation and return the internal value.
+
+        The provided data may be `empty` if no representation was included
+        in the input.
+
+        May raise `SkipField` if the field should not be included in the
+        validated data.
+        """
+        (is_empty_value, data) = self.validate_empty_values(data)
+        if is_empty_value:
+            return data
         value = self.to_internal_value(data)
         self.run_validators(value)
         return value
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -229,6 +229,35 @@ def __new__(cls, name, bases, attrs):
         return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs)
 
 
+def get_validation_error_detail(exc):
+    assert isinstance(exc, (ValidationError, DjangoValidationError))
+
+    if isinstance(exc, DjangoValidationError):
+        # Normally you should raise `serializers.ValidationError`
+        # inside your codebase, but we handle Django's validation
+        # exception class as well for simpler compat.
+        # Eg. Calling Model.clean() explicitly inside Serializer.validate()
+        return {
+            api_settings.NON_FIELD_ERRORS_KEY: list(exc.messages)
+        }
+    elif isinstance(exc.detail, dict):
+        # If errors may be a dict we use the standard {key: list of values}.
+        # Here we ensure that all the values are *lists* of errors.
+        return dict([
+            (key, value if isinstance(value, list) else [value])
+            for key, value in exc.detail.items()
+        ])
+    elif isinstance(exc.detail, list):
+        # Errors raised as a list are non-field errors.
+        return {
+            api_settings.NON_FIELD_ERRORS_KEY: exc.detail
+        }
+    # Errors raised as a string are non-field errors.
+    return {
+        api_settings.NON_FIELD_ERRORS_KEY: [exc.detail]
+    }
+
+
 @six.add_metaclass(SerializerMetaclass)
 class Serializer(BaseSerializer):
     default_error_messages = {
@@ -293,55 +322,17 @@ def run_validation(self, data=empty):
         performed by validators and the `.validate()` method should
         be coerced into an error dictionary with a 'non_fields_error' key.
         """
-        if data is empty:
-            if getattr(self.root, 'partial', False):
-                raise SkipField()
-            if self.required:
-                self.fail('required')
-            return self.get_default()
-
-        if data is None:
-            if not self.allow_null:
-                self.fail('null')
-            return None
-
-        if not isinstance(data, dict):
-            message = self.error_messages['invalid'].format(
-                datatype=type(data).__name__
-            )
-            raise ValidationError({
-                api_settings.NON_FIELD_ERRORS_KEY: [message]
-            })
+        (is_empty_value, data) = self.validate_empty_values(data)
+        if is_empty_value:
+            return data
 
         value = self.to_internal_value(data)
         try:
             self.run_validators(value)
             value = self.validate(value)
             assert value is not None, '.validate() should return the validated data'
-        except ValidationError as exc:
-            if isinstance(exc.detail, dict):
-                # .validate() errors may be a dict, in which case, use
-                # standard {key: list of values} style.
-                raise ValidationError(dict([
-                    (key, value if isinstance(value, list) else [value])
-                    for key, value in exc.detail.items()
-                ]))
-            elif isinstance(exc.detail, list):
-                raise ValidationError({
-                    api_settings.NON_FIELD_ERRORS_KEY: exc.detail
-                })
-            else:
-                raise ValidationError({
-                    api_settings.NON_FIELD_ERRORS_KEY: [exc.detail]
-                })
-        except DjangoValidationError as exc:
-            # Normally you should raise `serializers.ValidationError`
-            # inside your codebase, but we handle Django's validation
-            # exception class as well for simpler compat.
-            # Eg. Calling Model.clean() explicitly inside Serializer.validate()
-            raise ValidationError({
-                api_settings.NON_FIELD_ERRORS_KEY: list(exc.messages)
-            })
+        except (ValidationError, DjangoValidationError) as exc:
+            raise ValidationError(detail=get_validation_error_detail(exc))
 
         return value
 
@@ -349,6 +340,14 @@ def to_internal_value(self, data):
         """
         Dict of native values <- Dict of primitive datatypes.
         """
+        if not isinstance(data, dict):
+            message = self.error_messages['invalid'].format(
+                datatype=type(data).__name__
+            )
+            raise ValidationError({
+                api_settings.NON_FIELD_ERRORS_KEY: [message]
+            })
+
         ret = OrderedDict()
         errors = OrderedDict()
         fields = [
@@ -462,6 +461,26 @@ def get_value(self, dictionary):
             return html.parse_html_list(dictionary, prefix=self.field_name)
         return dictionary.get(self.field_name, empty)
 
+    def run_validation(self, data=empty):
+        """
+        We override the default `run_validation`, because the validation
+        performed by validators and the `.validate()` method should
+        be coerced into an error dictionary with a 'non_fields_error' key.
+        """
+        (is_empty_value, data) = self.validate_empty_values(data)
+        if is_empty_value:
+            return data
+
+        value = self.to_internal_value(data)
+        try:
+            self.run_validators(value)
+            value = self.validate(value)
+            assert value is not None, '.validate() should return the validated data'
+        except (ValidationError, DjangoValidationError) as exc:
+            raise ValidationError(detail=get_validation_error_detail(exc))
+
+        return value
+
     def to_internal_value(self, data):
         """
         List of dicts of native values <- List of dicts of primitive datatypes.
@@ -503,6 +522,9 @@ def to_representation(self, data):
             self.child.to_representation(item) for item in iterable
         ]
 
+    def validate(self, attrs):
+        return attrs
+
     def update(self, instance, validated_data):
         raise NotImplementedError(
             "Serializers with many=True do not support multiple update by "


Test patch:
diff --git a/tests/test_serializer_lists.py b/tests/test_serializer_lists.py
--- a/tests/test_serializer_lists.py
+++ b/tests/test_serializer_lists.py
@@ -272,3 +272,19 @@ def test_validate_html_input(self):
         serializer = self.Serializer(data=input_data)
         assert serializer.is_valid()
         assert serializer.validated_data == expected_output
+
+
+class TestListSerializerClass:
+    """Tests for a custom list_serializer_class."""
+    def test_list_serializer_class_validate(self):
+        class CustomListSerializer(serializers.ListSerializer):
+            def validate(self, attrs):
+                raise serializers.ValidationError('Non field error')
+
+        class TestSerializer(serializers.Serializer):
+            class Meta:
+                list_serializer_class = CustomListSerializer
+
+        serializer = TestSerializer(data=[], many=True)
+        assert not serializer.is_valid()
+        assert serializer.errors == {'non_field_errors': ['Non field error']}