Repository: encode/django-rest-framework

Issue:
Nested ListField serializer errors should include object reference
 When validation errors happen in nested ListField serializers -- an OrderedDict is returned for the ListField containing the nested errors.  However, it doesn't indicate which object in the list caused the error.  

For example, my nested serializer has a URLField, if I pass in a list of 10 objects -- the error doesn't indicate which of the 10 objects contains the error.  

``` python

class ChildSerializer(serializers.Serializer):
   url = serializers.URLField()
   uuid = serializers.UUIDField()
   other_fields = serializers.CharField() # many more...

class MySerializer(serializers.ModelSerializer):
    # my_nested_list_field is a JSONfield on the Django model
    my_nested_list_field = serializer.ListField(child=ChildSerializer())

data = {#other fields...,
            'my_nested_listfield': [
           {"url": "some invalid url", "other_fields": "MANY"},
           {"url": "http://www.google.com/", "other_fields": "MANY"},
            #9 (n) more dicts...
            ]
}

x = MySerializer(data=data)
x.is_valid()

x.errors
{'my_nested_listfield': OrderedDict([('url', [u'Enter a valid URL.'])])}
# did this error occur in dict #1, #5, #10????  Start trawling through the list looking for the bad URL...
```

Nested errors should probably contain some metadata to indicate which item(s) failed validation so that useful errors can be displayed to the client.

Something like:

``` python
{'my_nested_listfield': OrderedDict([('url', [u'Enter a valid URL.'] "_meta_ref": "OBJECT_123")])}
# Oh, I see, the error occured on OBJECT_123 -- this will be quick and easy to debug
```

Where `_meta_ref` is generated by an overrideable method on the child serializer.  It could default to the object `url` -- but in this case I would use an internal id that is meaningful to my application, like the `uuid`



Discussion:
Get the general idea of this, but not clear what a nice way to do this would be.

I'm not super keen on `_meta_ref` but don't have any great alternative.


Resolving patch:
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -1626,7 +1626,7 @@ def to_internal_value(self, data):
             self.fail('not_a_list', input_type=type(data).__name__)
         if not self.allow_empty and len(data) == 0:
             self.fail('empty')
-        return [self.child.run_validation(item) for item in data]
+        return self.run_child_validation(data)
 
     def to_representation(self, data):
         """
@@ -1634,6 +1634,20 @@ def to_representation(self, data):
         """
         return [self.child.to_representation(item) if item is not None else None for item in data]
 
+    def run_child_validation(self, data):
+        result = []
+        errors = OrderedDict()
+
+        for idx, item in enumerate(data):
+            try:
+                result.append(self.child.run_validation(item))
+            except ValidationError as e:
+                errors[idx] = e.detail
+
+        if not errors:
+            return result
+        raise ValidationError(errors)
+
 
 class DictField(Field):
     child = _UnvalidatedField()
@@ -1669,10 +1683,7 @@ def to_internal_value(self, data):
             data = html.parse_html_dict(data)
         if not isinstance(data, dict):
             self.fail('not_a_dict', input_type=type(data).__name__)
-        return {
-            six.text_type(key): self.child.run_validation(value)
-            for key, value in data.items()
-        }
+        return self.run_child_validation(data)
 
     def to_representation(self, value):
         """
@@ -1683,6 +1694,22 @@ def to_representation(self, value):
             for key, val in value.items()
         }
 
+    def run_child_validation(self, data):
+        result = {}
+        errors = OrderedDict()
+
+        for key, value in data.items():
+            key = six.text_type(key)
+
+            try:
+                result[key] = self.child.run_validation(value)
+            except ValidationError as e:
+                errors[key] = e.detail
+
+        if not errors:
+            return result
+        raise ValidationError(errors)
+
 
 class JSONField(Field):
     default_error_messages = {


Test patch:
diff --git a/tests/test_fields.py b/tests/test_fields.py
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -1767,7 +1767,7 @@ class TestListField(FieldValues):
     ]
     invalid_inputs = [
         ('not a list', ['Expected a list of items but got type "str".']),
-        ([1, 2, 'error'], ['A valid integer is required.']),
+        ([1, 2, 'error', 'error'], {2: ['A valid integer is required.'], 3: ['A valid integer is required.']}),
         ({'one': 'two'}, ['Expected a list of items but got type "dict".'])
     ]
     outputs = [
@@ -1794,6 +1794,25 @@ def test_collection_types_are_invalid_input(self):
         assert exc_info.value.detail == ['Expected a list of items but got type "dict".']
 
 
+class TestNestedListField(FieldValues):
+    """
+    Values for nested `ListField` with IntegerField as child.
+    """
+    valid_inputs = [
+        ([[1, 2], [3]], [[1, 2], [3]]),
+        ([[]], [[]])
+    ]
+    invalid_inputs = [
+        (['not a list'], {0: ['Expected a list of items but got type "str".']}),
+        ([[1, 2, 'error'], ['error']], {0: {2: ['A valid integer is required.']}, 1: {0: ['A valid integer is required.']}}),
+        ([{'one': 'two'}], {0: ['Expected a list of items but got type "dict".']})
+    ]
+    outputs = [
+        ([[1, 2], [3]], [[1, 2], [3]]),
+    ]
+    field = serializers.ListField(child=serializers.ListField(child=serializers.IntegerField()))
+
+
 class TestEmptyListField(FieldValues):
     """
     Values for `ListField` with allow_empty=False flag.
@@ -1834,13 +1853,13 @@ class TestUnvalidatedListField(FieldValues):
 
 class TestDictField(FieldValues):
     """
-    Values for `ListField` with CharField as child.
+    Values for `DictField` with CharField as child.
     """
     valid_inputs = [
         ({'a': 1, 'b': '2', 3: 3}, {'a': '1', 'b': '2', '3': '3'}),
     ]
     invalid_inputs = [
-        ({'a': 1, 'b': None}, ['This field may not be null.']),
+        ({'a': 1, 'b': None, 'c': None}, {'b': ['This field may not be null.'], 'c': ['This field may not be null.']}),
         ('not a dict', ['Expected a dictionary of items but got type "str".']),
     ]
     outputs = [
@@ -1866,9 +1885,26 @@ def test_allow_null(self):
         assert output is None
 
 
+class TestNestedDictField(FieldValues):
+    """
+    Values for nested `DictField` with CharField as child.
+    """
+    valid_inputs = [
+        ({0: {'a': 1, 'b': '2'}, 1: {3: 3}}, {'0': {'a': '1', 'b': '2'}, '1': {'3': '3'}}),
+    ]
+    invalid_inputs = [
+        ({0: {'a': 1, 'b': None}, 1: {'c': None}}, {'0': {'b': ['This field may not be null.']}, '1': {'c': ['This field may not be null.']}}),
+        ({0: 'not a dict'}, {'0': ['Expected a dictionary of items but got type "str".']}),
+    ]
+    outputs = [
+        ({0: {'a': 1, 'b': '2'}, 1: {3: 3}}, {'0': {'a': '1', 'b': '2'}, '1': {'3': '3'}}),
+    ]
+    field = serializers.DictField(child=serializers.DictField(child=serializers.CharField()))
+
+
 class TestDictFieldWithNullChild(FieldValues):
     """
-    Values for `ListField` with allow_null CharField as child.
+    Values for `DictField` with allow_null CharField as child.
     """
     valid_inputs = [
         ({'a': None, 'b': '2', 3: 3}, {'a': None, 'b': '2', '3': '3'}),
@@ -1883,7 +1919,7 @@ class TestDictFieldWithNullChild(FieldValues):
 
 class TestUnvalidatedDictField(FieldValues):
     """
-    Values for `ListField` with no `child` argument.
+    Values for `DictField` with no `child` argument.
     """
     valid_inputs = [
         ({'a': 1, 'b': [4, 5, 6], 1: 123}, {'a': 1, 'b': [4, 5, 6], '1': 123}),