Repository: pydantic/pydantic

Issue:
Optional typecasting
I tried out Pydantic to see if my data had the right type, but I was surprised that it cast the data to the right type. I would expect an error to be raised if the value is a list (say), instead I got the string representation of that list.  For example, I found this string validator:

```python
def str_validator(v) -> str:
    if isinstance(v, (str, NoneType)):
        return v
    elif isinstance(v, bytes):
        return v.decode()
    return str(v)  # <-- Should this raise a validation error, or cast to a string?
```

Maybe this is intended behaviour, but I think it should raise a validation error. Maybe a `strict` option can be added to the `Config` class?


Discussion:
The problem is that it's fairly subjective what should be converted to a string:
* `bytes`: most people would probably say yes
* `int`: again probably yes
* `float`: a bit more dubious
* `True`, `False`: some would say no but others might want it
* list, dict, set: as you say this is probably unexpected but there are situations where it might be the easiest solution.

I'm fairly concerned about performance so I didn't want to add lots of type checks to a validator like `str_validator` which is used a lot.

That said,
```python
elif isinstance(v, (list, dict, set)):
    raise ValueError('...')
```
would seem pretty simple and probably wouldn't have a big performance impact.

The other option would be to add a validate method to your model `def validate_<field name>(self, v)`. (warning: I'm going to be changing how validators work a lot at some point, so this will probably break at some point but won't be hard to fix.)

Or create a custom type

```python
class StrictStr(str):
    @classmethod
    def get_validators(cls):
        yield cls.check_not_list
        yield str_validator
```
Simplest option would be to add `StrictStr` to pydantic so people can use that when required. That would be explicit and require the minimum change.

Resolving patch:
diff --git a/pydantic/types.py b/pydantic/types.py
--- a/pydantic/types.py
+++ b/pydantic/types.py
@@ -9,6 +9,7 @@
     'NoneBytes',
     'StrBytes',
     'NoneStrBytes',
+    'StrictStr',
     'ConstrainedStr',
     'constr',
     'EmailStr',
@@ -27,6 +28,18 @@
 NoneStrBytes = Optional[StrBytes]
 
 
+class StrictStr(str):
+    @classmethod
+    def get_validators(cls):
+        yield cls.validate
+
+    @classmethod
+    def validate(cls, v):
+        if not isinstance(v, str):
+            raise ValueError(f'strict string: str expected not {type(v)}')
+        return v
+
+
 class ConstrainedStr(str):
     min_length: int = None
     max_length: int = None
diff --git a/pydantic/validators.py b/pydantic/validators.py
--- a/pydantic/validators.py
+++ b/pydantic/validators.py
@@ -1,5 +1,6 @@
 from collections import OrderedDict
 from datetime import date, datetime, time, timedelta
+from decimal import Decimal
 from enum import Enum
 from pathlib import Path
 from typing import Any
@@ -19,9 +20,13 @@ def not_none_validator(v):
 def str_validator(v) -> str:
     if isinstance(v, (str, NoneType)):
         return v
-    elif isinstance(v, bytes):
+    elif isinstance(v, (bytes, bytearray)):
         return v.decode()
-    return str(v)
+    elif isinstance(v, (float, int, Decimal)):
+        # is there anything else we want to add here? If you think so, create an issue.
+        return str(v)
+    else:
+        raise ValueError(f'str or byte type expected not {type(v)}')
 
 
 def bytes_validator(v) -> bytes:


Test patch:
diff --git a/tests/test_complex.py b/tests/test_complex.py
--- a/tests/test_complex.py
+++ b/tests/test_complex.py
@@ -1,3 +1,4 @@
+from decimal import Decimal
 from enum import Enum
 from typing import Any, Dict, List, Set, Union
 
@@ -340,10 +341,12 @@ class Model(BaseModel):
     assert len(Model.__fields__['v'].sub_fields[0].sub_fields[0].sub_fields) == 2
 
 
+class StrEnum(str, Enum):
+    a = 'a10'
+    b = 'b10'
+
+
 def test_str_enum():
-    class StrEnum(str, Enum):
-        a = 'a10'
-        b = 'b10'
 
     class Model(BaseModel):
         v: StrEnum = ...
@@ -445,3 +448,35 @@ class Model(BaseModel):
             x: 43 = 123
 
     assert "error checking inheritance of 43 (type: <class 'int'>)" in str(exc_info)
+
+
+@pytest.mark.parametrize('value,expected', [
+    ('a string', 'a string'),
+    (b'some bytes', 'some bytes'),
+    (bytearray('foobar', encoding='utf8'), 'foobar'),
+    (123, '123'),
+    (123.45, '123.45'),
+    (Decimal('12.45'), '12.45'),
+    (True, 'True'),
+    (False, 'False'),
+    (StrEnum.a, 'a10'),
+])
+def test_valid_string_types(value, expected):
+    class Model(BaseModel):
+        v: str
+
+    assert Model(v=value).v == expected
+
+
+@pytest.mark.parametrize('value', [
+    {'foo': 'bar'},
+    {'foo', 'bar'},
+    [1, 2, 3],
+])
+def test_invalid_string_types(value):
+    class Model(BaseModel):
+        v: str
+
+    with pytest.raises(ValidationError) as exc_info:
+        Model(v=value)
+    assert 'str or byte type expected' in str(exc_info.value)
diff --git a/tests/test_types.py b/tests/test_types.py
--- a/tests/test_types.py
+++ b/tests/test_types.py
@@ -5,8 +5,8 @@
 
 import pytest
 
-from pydantic import (DSN, BaseModel, EmailStr, NameEmail, NegativeInt, PositiveInt, PyObject, ValidationError, conint,
-                      constr)
+from pydantic import (DSN, BaseModel, EmailStr, NameEmail, NegativeInt, PositiveInt, PyObject, StrictStr,
+                      ValidationError, conint, constr)
 
 
 class ConStringModel(BaseModel):
@@ -378,3 +378,15 @@ class SetModel(BaseModel):
     assert m.v == {1, 2, 3}
     assert m.values() == {'v': {1, 2, 3}}
     assert SetModel(v={'a', 'b', 'c'}).v == {'a', 'b', 'c'}
+
+
+def test_strict_str():
+    class Model(BaseModel):
+        v: StrictStr
+
+    assert Model(v='foobar').v == 'foobar'
+    with pytest.raises(ValidationError):
+        Model(v=123)
+
+    with pytest.raises(ValidationError):
+        Model(v=b'foobar')