Index: docs/releases/1.4.txt
===================================================================
--- docs/releases/1.4.txt	(revision 17075)
+++ docs/releases/1.4.txt	(working copy)
@@ -477,6 +477,9 @@
   This should make it easier to read when debugging interaction with
   client-side Javascript code.
 
+* :meth:`Model.clean()` can raise :exc:`~django.core.exceptions.ValidationError` with
+  a dictionary, permitting validation errors on individual fields.
+
 .. _backwards-incompatible-changes-1.4:
 
 Backwards incompatible changes in 1.4
Index: docs/ref/models/instances.txt
===================================================================
--- docs/ref/models/instances.txt	(revision 17075)
+++ docs/ref/models/instances.txt	(working copy)
@@ -115,6 +115,24 @@
     except ValidationError, e:
         non_field_errors = e.message_dict[NON_FIELD_ERRORS]
 
+.. versionadded:: 1.4
+   :meth:`Model.clean()` recognizes :exc:`~django.core.exceptions.ValidationError` with a
+   dictionary.
+
+Alternatively, it is possible to specify errors for individual fields by providing
+a dictionary to the :exc:`~django.core.exceptions.ValidationError` constructor that maps field names to messages::
+
+    def clean(self):
+        from collections import defaultdict
+        from django.core.exceptions import ValidationError
+        
+        message_dict = defaultdict(list)
+        if self.min and self.max and (not self.min <= self.max):
+            message_dict['min'].append(u'Min value must be less that or equal to the max value.')
+            message_dict['max'].append(u'Max value must be greater than or equal to the min value.')
+        if len(message_dict):
+            raise ValidationError(message_dict)
+
 Finally, ``full_clean()`` will check any unique constraints on your model.
 
 .. method:: Model.validate_unique(exclude=None)
Index: django/forms/models.py
===================================================================
--- django/forms/models.py	(revision 17075)
+++ django/forms/models.py	(working copy)
@@ -331,7 +331,10 @@
         try:
             self.instance.clean()
         except ValidationError, e:
-            self._update_errors({NON_FIELD_ERRORS: e.messages})
+            if e.has_message_dict:
+                self._update_errors(e.message_dict)
+            else:
+                self._update_errors({NON_FIELD_ERRORS: e.messages})
 
         # Validate uniqueness if needed.
         if self._validate_unique:
Index: django/core/exceptions.py
===================================================================
--- django/core/exceptions.py	(revision 17075)
+++ django/core/exceptions.py	(working copy)
@@ -59,23 +59,27 @@
             self.params = params
             message = force_unicode(message)
             self.messages = [message]
-
+    
+    @property
+    def has_message_dict(self):
+        return hasattr(self, 'message_dict')
+    
     def __str__(self):
         # This is needed because, without a __str__(), printing an exception
         # instance would result in this:
         # AttributeError: ValidationError instance has no attribute 'args'
         # See http://www.python.org/doc/current/tut/node10.html#handling
-        if hasattr(self, 'message_dict'):
+        if self.has_message_dict:
             return repr(self.message_dict)
         return repr(self.messages)
 
     def __repr__(self):
-        if hasattr(self, 'message_dict'):
+        if self.has_message_dict:
             return 'ValidationError(%s)' % repr(self.message_dict)
         return 'ValidationError(%s)' % repr(self.messages)
 
     def update_error_dict(self, error_dict):
-        if hasattr(self, 'message_dict'):
+        if self.has_message_dict:
             if error_dict:
                 for k, v in self.message_dict.items():
                     error_dict.setdefault(k, []).extend(v)
Index: tests/modeltests/validation/tests.py
===================================================================
--- tests/modeltests/validation/tests.py	(revision 17075)
+++ tests/modeltests/validation/tests.py	(working copy)
@@ -11,7 +11,7 @@
 
 from . import ValidationTestCase
 from .models import (Author, Article, ModelToValidate,
-    GenericIPAddressTestModel, GenericIPAddrUnpackUniqueTest)
+    GenericIPAddressTestModel, GenericIPAddrUnpackUniqueTest, Range)
 
 # Import other tests for this package.
 from .test_custom_messages import CustomMessagesTest
@@ -196,3 +196,31 @@
         giptm.save()
         giptm = GenericIPAddrUnpackUniqueTest(generic_v4unpack_ip="18.52.18.52")
         self.assertFailsValidation(giptm.full_clean, ['generic_v4unpack_ip',])
+
+class RangeForm(forms.ModelForm):
+    class Meta:
+        model = Range
+
+class ModelCleanPerFieldValidationTests(ValidationTestCase):
+    
+    def test_no_errors(self):
+        range = Range(min=0, max=1)
+        range.full_clean()
+        
+        form = RangeForm(instance=range)
+        form.full_clean()
+        self.assertEqual({}, form.errors)
+    
+    def test_two_errors(self):
+        range = Range(min=999, max=1)
+        self.assertFieldFailsValidationWithMessage(range.full_clean, 'min', [u'Min value must be less that or equal to the max value.'])
+        self.assertFieldFailsValidationWithMessage(range.full_clean, 'max', [u'Max value must be greater than or equal to the min value.'])
+        
+        # In particular, the key should not be '__all__'
+        expected_error_dict = {
+            'min': [u'Min value must be less that or equal to the max value.'],
+            'max': [u'Max value must be greater than or equal to the min value.'],
+        }
+        form = RangeForm(data={'min': 999, 'max': 1})
+        form.full_clean()
+        self.assertEqual(expected_error_dict, form.errors)
Index: tests/modeltests/validation/models.py
===================================================================
--- tests/modeltests/validation/models.py	(revision 17075)
+++ tests/modeltests/validation/models.py	(working copy)
@@ -1,3 +1,4 @@
+from collections import defaultdict
 from datetime import datetime
 
 from django.core.exceptions import ValidationError
@@ -92,6 +93,17 @@
 class GenericIPAddrUnpackUniqueTest(models.Model):
     generic_v4unpack_ip = models.GenericIPAddressField(blank=True, unique=True, unpack_ipv4=True)
 
+class Range(models.Model):
+    min = models.IntegerField()
+    max = models.IntegerField()
+    
+    def clean(self):
+        message_dict = defaultdict(list)
+        if self.min and self.max and (not self.min <= self.max):
+            message_dict['min'].append(u'Min value must be less that or equal to the max value.')
+            message_dict['max'].append(u'Max value must be greater than or equal to the min value.')
+        if len(message_dict):
+            raise ValidationError(message_dict)
 
 # A model can't have multiple AutoFields
 # Refs #12467.
@@ -102,4 +114,4 @@
         auto2 = models.AutoField(primary_key=True)
 except AssertionError, assertion_error:
     pass # Fail silently
-assert str(assertion_error) == u"A model can't have more than one AutoField."
\ No newline at end of file
+assert str(assertion_error) == u"A model can't have more than one AutoField."
