Repository: encode/django-rest-framework

Issue:
CharField should not accept collections as valid input
This issue came up on the marshmallow issue tracker (https://github.com/marshmallow-code/marshmallow/issues/231), so I thought I'd pass it along here.

It seems incorrect for `CharField` to take numbers and collections as valid input.
### Code to reproduce

``` python
from django.conf import settings
settings.configure(INSTALLED_APPS=['rest_framework'])
import django
django.setup()
from rest_framework import serializers as ser
from rest_framework.exceptions import ValidationError

field = ser.CharField()

inputs = (
    42,
    42.0,
    {},
    []
)

for val in inputs:
    try:
        field.run_validation(val)
    except ValidationError as err:
        print(err.detail[0])
```
### Expected

Validation fails with messages like `"{} is not a valid string."`.
### Actual

Numbers and collections pass validation.



Discussion:
I'm not quite sure why the py3x-django18 builds are failing; I can look into it further if you decide this change is a good idea.

I don't think I mind us coercing numbers to string representations. Collection types should prob fail tho.

I second @tomchristie 

What is the rationale for allowing numbers? Any time a client passes a number to a string field, it is most likely due to user or client error and should be treated as such. The client should be responsible for "stringifying" any numerical input.

^ Same goes for booleans passed to a string field.

I agree with triggering an error when a collection is passed in as a string.

> What is the rationale for allowing numbers?

I'm -0 on not allowing numbers _mostly_ because that's expected functionality at the moment and changing this would break backwards compatibility. We even had a test to ensure that numbers were coerced properly.

It's also useful to note that there may be renders out there which try their best to coerce string input to the right type, so numbers would become a numeric type (decimal, probably) instead of staying as strings. We allow for the reverse case (numbers passed in as strings to number-type fields), so I don't understand why we would want to disallow this.

> We allow for the reverse case (numbers passed in as strings to number-type fields), so I don't understand why we would want to disallow this.

Indeed, most of simple fields accept strings as input. The fields' deserialization logic, however, is responsible for deciding which string inputs are valid (e.g. `Boolean` accepts `"true"` but not `"glue"`). That logic is also responsible for deciding which Python types are valid (e.g. `Dict` only accepts `dict`; `Boolean` doesn't accept `dict`).

I lean slightly toward disallowing numbers as inputs to `String`. That said, I don't feel that strongly about it, and it's probably not worth breaking backwards compat for this change. Let me know how to proceed.

At least they should be treated as individually. The case for one pull request is unambiguous, the case for the other less so.

I'd like to see this be configurable at least. I would imagine passing anything other than a string to a CharField is usually indicative of user error.

> I would imagine passing anything other than a string to a CharField is usually indicative of user error.

Possibly, tho "Generous on input, strict on output" could also be a valid rule of thumb for us to use here.

Adding an option to coerce input to string sounds valid to me. I don't think we should coerce collections though.


Resolving patch:
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -672,6 +672,7 @@ def to_representation(self, value):
 
 class CharField(Field):
     default_error_messages = {
+        'invalid': _('Not a valid string.'),
         'blank': _('This field may not be blank.'),
         'max_length': _('Ensure this field has no more than {max_length} characters.'),
         'min_length': _('Ensure this field has at least {min_length} characters.')
@@ -702,6 +703,11 @@ def run_validation(self, data=empty):
         return super(CharField, self).run_validation(data)
 
     def to_internal_value(self, data):
+        # We're lenient with allowing basic numerics to be coerced into strings,
+        # but other types should fail. Eg. unclear if booleans should represent as `true` or `True`,
+        # and composites such as lists are likely user error.
+        if isinstance(data, bool) or not isinstance(data, six.string_types + six.integer_types + (float,)):
+            self.fail('invalid')
         value = six.text_type(data)
         return value.strip() if self.trim_whitespace else value
 
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -168,7 +168,7 @@ def get_choices(self, cutoff=None):
 
         return OrderedDict([
             (
-                six.text_type(self.to_representation(item)),
+                self.to_representation(item),
                 self.display_value(item)
             )
             for item in queryset


Test patch:
diff --git a/tests/test_fields.py b/tests/test_fields.py
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -535,6 +535,8 @@ class TestCharField(FieldValues):
         'abc': 'abc'
     }
     invalid_inputs = {
+        (): ['Not a valid string.'],
+        True: ['Not a valid string.'],
         '': ['This field may not be blank.']
     }
     outputs = {
diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py
--- a/tests/test_model_serializer.py
+++ b/tests/test_model_serializer.py
@@ -614,7 +614,7 @@ class Meta:
                 fields = '__all__'
 
         serializer = TestSerializer()
-        expected = OrderedDict([('1', 'Red Color'), ('2', 'Yellow Color'), ('3', 'Green Color')])
+        expected = OrderedDict([(1, 'Red Color'), (2, 'Yellow Color'), (3, 'Green Color')])
         self.assertEqual(serializer.fields['color'].choices, expected)
 
     def test_custom_display_value(self):
@@ -630,7 +630,7 @@ class Meta:
                 fields = '__all__'
 
         serializer = TestSerializer()
-        expected = OrderedDict([('1', 'My Red Color'), ('2', 'My Yellow Color'), ('3', 'My Green Color')])
+        expected = OrderedDict([(1, 'My Red Color'), (2, 'My Yellow Color'), (3, 'My Green Color')])
         self.assertEqual(serializer.fields['color'].choices, expected)