Repository: pydantic/pydantic

Issue:
Broken loading list of tuples.
# Bug

Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:
```
pydantic version: 1.4
pydantic compiled: True
install path: /home/**removed**/venv/lib/python3.7/site-packages/pydantic
python version: 3.7.3 (default, Apr  3 2019, 19:16:38)  [GCC 8.0.1 20180414 (experimental) [trunk revision 259383]]
platform: Linux-4.19.86-041986-generic-x86_64-with-Ubuntu-18.04-bionic
     optional deps. installed: []
```

There is pretty strange behaviour on loading nested list of tuples. I firstly think that this might be intended, but then found out that parse_obj and parse_obj_as give different execution flow which  frustrates me.

```py
from pydantic import BaseModel

class OperationData(BaseModel):
    id: str

class Operation(BaseModel):
    __root__: Tuple[int, OperationData]

data = [0, {'id': '1.11.0'}]
# this one works as expected
print(Operation.parse_obj(data))
# printed: __root__=(0, OperationData(id='1.11.0'))

# However, this one doesn't
print(parse_obj_as(Operation, data))
# Traceback (most recent call last):
#  File "/home/**removed**/protocol/base.py", line 238, in <module>
#    print(parse_obj_as(Operation, data))
#  File "pydantic/tools.py", line 35, in pydantic.tools.parse_obj_as
#  File "pydantic/main.py", line 283, in pydantic.main.BaseModel.__init__
# pydantic.error_wrappers.ValidationError: 1 validation error for ParsingModel[Operation]
#__root__
# value is not a valid dict (type=type_error.dict)

# Which is not a big problem. The problem is that I have nested class

class OperationsBatch(BaseModel):
    batch_desc: str
    operations: List[Operation]

# and it produces same exception on
print(OperationsBatch.parse_obj({'batch_desc': '123', 'operations': [data, data]}))

# Traceback (most recent call last):
# File "/home/**removed**/protocol/base.py", line 243, in <module>
#    OperationsBatch.parse_obj({'batch_desc': '123', 'operations': [data, data]})
#  File "pydantic/main.py", line 402, in pydantic.main.BaseModel.parse_obj
#  File "pydantic/main.py", line 283, in pydantic.main.BaseModel.__init__
# pydantic.error_wrappers.ValidationError: 2 validation errors for OperationsBatch
# operations -> 0
# value is not a valid dict (type=type_error.dict)
# operations -> 1
# value is not a valid dict (type=type_error.dict)

```

It doesn't look like a right behaviour.



Discussion:
I suspect this is because internally `parse_obj_as` creates a model with `__root__` set to its first argument, so effectively you're doing

```py
from pydantic import BaseModel

class OperationData(BaseModel):
    id: str

class Operation(BaseModel):
    __root__: Tuple[int, OperationData]

class ParseAsObjectModel(BaseModel):
    __root__: Operation
```

which then fails, should be fixable via either or both:
1. getting the above to work, even though it looks weird
2. `parse_as_obj` detecting a model (perhaps a model with a custom root type) as it's first argument and using that model instead of creating another one.

It occurs to me that 1. might not be possible (or possible without complex or backwards incompatible changes) due to the way we handle custom root types.

The second option therefore might be easier.
For what it's worth, the following change seems to make the error go away:
Change:

https://github.com/samuelcolvin/pydantic/blob/e4cd9d2c87b0e6f42366b617de6441f8cd561085/pydantic/main.py#L549-L554

to

```python
        else:
            if cls.__custom_root_type__:
                value_as_dict = {"__root__": value}
            else:
                try:
                    value_as_dict = dict(value)
                except (TypeError, ValueError) as e:
                    raise DictError() from e
            return cls(**value_as_dict)
```

I don't necessarily think this is the optimal fix, but it might be useful as a starting point.

Resolving patch:
diff --git a/pydantic/main.py b/pydantic/main.py
--- a/pydantic/main.py
+++ b/pydantic/main.py
@@ -546,6 +546,8 @@ def validate(cls: Type['Model'], value: Any) -> 'Model':
             return value.copy()
         elif cls.__config__.orm_mode:
             return cls.from_orm(value)
+        elif cls.__custom_root_type__:
+            return cls.parse_obj(value)
         else:
             try:
                 value_as_dict = dict(value)


Test patch:
diff --git a/tests/test_parse.py b/tests/test_parse.py
--- a/tests/test_parse.py
+++ b/tests/test_parse.py
@@ -1,10 +1,10 @@
 import json
 import pickle
-from typing import List, Union
+from typing import List, Tuple, Union
 
 import pytest
 
-from pydantic import BaseModel, Field, Protocol, ValidationError
+from pydantic import BaseModel, Field, Protocol, ValidationError, parse_obj_as
 
 
 class Model(BaseModel):
@@ -57,6 +57,55 @@ class MyModel(BaseModel):
     assert m.__root__ == ['a']
 
 
+def test_parse_nested_root_list():
+    class NestedData(BaseModel):
+        id: str
+
+    class NestedModel(BaseModel):
+        __root__: List[NestedData]
+
+    class MyModel(BaseModel):
+        nested: NestedModel
+
+    m = MyModel.parse_obj({'nested': [{'id': 'foo'}]})
+    assert isinstance(m.nested, NestedModel)
+    assert isinstance(m.nested.__root__[0], NestedData)
+
+
+def test_parse_nested_root_tuple():
+    class NestedData(BaseModel):
+        id: str
+
+    class NestedModel(BaseModel):
+        __root__: Tuple[int, NestedData]
+
+    class MyModel(BaseModel):
+        nested: List[NestedModel]
+
+    data = [0, {'id': 'foo'}]
+    m = MyModel.parse_obj({'nested': [data]})
+    assert isinstance(m.nested[0], NestedModel)
+    assert isinstance(m.nested[0].__root__[1], NestedData)
+
+    nested = parse_obj_as(NestedModel, data)
+    assert isinstance(nested, NestedModel)
+
+
+def test_parse_nested_custom_root():
+    class NestedModel(BaseModel):
+        __root__: List[str]
+
+    class MyModel(BaseModel):
+        __root__: NestedModel
+
+    nested = ['foo', 'bar']
+    m = MyModel.parse_obj(nested)
+    assert isinstance(m, MyModel)
+    assert isinstance(m.__root__, NestedModel)
+    assert isinstance(m.__root__.__root__, List)
+    assert isinstance(m.__root__.__root__[0], str)
+
+
 def test_json():
     assert Model.parse_raw('{"a": 12, "b": 8}') == Model(a=12, b=8)