Repository: pydantic/pydantic

Issue:
Allow dict like types to __root__
Hi @samuelcolvin , Thank you for this amazing library. I am using it with fastapi for defining input models.
```
"preview": {
		"9:16": {
			"thumbnail": "",
			"video": ""
		}, "16:9": {
			"thumbnail": "",
			"video": ""
		}, "1:1": {
			"thumbnail": "",
			"video": ""
		}
	}
```
I have the above data coming in the request for which I want to create a model. I tried implementing wildcard fields but didn't get any success. In the above case, the aspect ratio is dynamic and can be anything but will always be in *:* format. In case that is coming, thumbnail and video are in nested model.

I have implemented this in JSON schema like the following:
```
template_schema = {
    "type": "object",
    "properties": {
        "preview": {
            "minProperties": 1,
            "type": "object",
            "patternProperties": {
               "^[0-9]{1,2}:\d{1,2}$": {
                    "type": "object",
                    "properties": {
                        "thumbnail": {"type": "string"},
                        "video": {"type": "string"}
                    },
                    "required": ["thumbnail", "video"],
                }
            },
        },
    }
}```

Please guide me on how to get it to work? 

Any leads would be appreciated. Thanks in advance!


Discussion:
If you want change the input data to confirm to a more conventional shape, you'd be best of with something like:

```py
class ChildModel(BaseModel):
    thumbnail: str
    video: str

class MainModel(BaseModel):
    __root__: Dict[str, ChildModel]
```
Then you'll need a validator to check the format of the keys.

You'll then need to manually [customise the schema](https://pydantic-docs.helpmanual.io/usage/schema/#schema-customization)
    
Hi @samuelcolvin, thank you for reverting back so quick. I tried to follow the link and the example shared you above. but wasn't able to achieve the use case as mentioned above. Can you please help me with this? A code example will be a great help. 

I get the following error using your code snippet
`TypeError: custom root type cannot allow mapping`

Thanks in advance!

Bump!
Sorry, I forgot dictionaries can't be used as the root type.

You need to use a key for the dictionaries of video formats, in your above example that's fine since you have `{"preview: {...}}`.

With that you might want something like:

```py
from typing import Dict
from pydantic import BaseModel, validator
from devtools import debug

class ChildModel(BaseModel):
    thumbnail: str
    video: str

class MainModel(BaseModel):
    preview: Dict[str, ChildModel]

    @validator('preview')
    def check_keys(cls, v):
        for i, k in enumerate(v):
            if ':' not in k:
                raise ValueError(f'{i} key {k!r} does not contain a colon (:)')
        return v


data = {
    'preview': {
        '9:16': {'thumbnail': '', 'video': ''},
        '16:9': {'thumbnail': '', 'video': ''},
        '1:1': {'thumbnail': '', 'video': ''},
        'bad': {'thumbnail': '', 'video': ''},
    }
}
m = MainModel(**data)
debug(m)
```
Will raise an error as one of the keys doesn't contain a colon.

If you're data is just a dictionary of the form `{"9:16": {}, "16:9": {}, ...}`, you would need to initialise with something like `MainModel(preview=data)`.
@samuelcolvin Is there a fundamental reason we don't support root mappings, or is it just because this way was easier to implement for now? I recognize it may require adding an unacceptable amount of added complexity to support a mapping type in the root; I'm just trying to understand whether this restriction is more purposeful or accidental.
I think the problem is `parse_obj`:

https://github.com/samuelcolvin/pydantic/blob/f5cde39e7550871e6ca1aceb70891a4bf9f01608/pydantic/main.py#L356

Might be possible to fix, but that was why we did it.
Yeah, I think it could make sense to replace that with a root model check; it feels kind of arbitrarily limiting to not support mappings as the root model.

On the other hand, I don't love adding all of these runtime checks when in most cases they aren't relevant; maybe there is a way to essentially decorate/replace this method inside the metaclass to get different behavior (if the model is a custom root model with a mapping type)?

(I guess maybe this is starting to get outside the scope of this issue though.)

Resolving patch:
diff --git a/docs/examples/models_custom_root_field_parse_obj.py b/docs/examples/models_custom_root_field_parse_obj.py
new file mode 100644
--- /dev/null
+++ b/docs/examples/models_custom_root_field_parse_obj.py
@@ -0,0 +1,17 @@
+from typing import List, Dict
+from pydantic import BaseModel, ValidationError
+
+class Pets(BaseModel):
+    __root__: List[str]
+
+print(Pets.parse_obj(['dog', 'cat']))
+print(Pets.parse_obj({'__root__': ['dog', 'cat']}))  # not recommended
+
+class PetsByName(BaseModel):
+    __root__: Dict[str, str]
+
+print(PetsByName.parse_obj({'Otis': 'dog', 'Milo': 'cat'}))
+try:
+    PetsByName.parse_obj({'__root__': {'Otis': 'dog', 'Milo': 'cat'}})
+except ValidationError as e:
+    print(e)
diff --git a/pydantic/main.py b/pydantic/main.py
--- a/pydantic/main.py
+++ b/pydantic/main.py
@@ -136,8 +136,6 @@ def is_valid_field(name: str) -> bool:
 def validate_custom_root_type(fields: Dict[str, ModelField]) -> None:
     if len(fields) > 1:
         raise ValueError('__root__ cannot be mixed with other fields')
-    if fields[ROOT_KEY].shape == SHAPE_MAPPING:
-        raise TypeError('custom root type cannot allow mapping')
 
 
 UNTOUCHED_TYPES = FunctionType, property, type, classmethod, staticmethod
@@ -382,15 +380,16 @@ def json(
 
     @classmethod
     def parse_obj(cls: Type['Model'], obj: Any) -> 'Model':
-        if not isinstance(obj, dict):
-            if cls.__custom_root_type__:
-                obj = {ROOT_KEY: obj}
-            else:
-                try:
-                    obj = dict(obj)
-                except (TypeError, ValueError) as e:
-                    exc = TypeError(f'{cls.__name__} expected dict not {type(obj).__name__}')
-                    raise ValidationError([ErrorWrapper(exc, loc=ROOT_KEY)], cls) from e
+        if cls.__custom_root_type__ and (
+            not (isinstance(obj, dict) and obj.keys() == {ROOT_KEY}) or cls.__fields__[ROOT_KEY].shape == SHAPE_MAPPING
+        ):
+            obj = {ROOT_KEY: obj}
+        elif not isinstance(obj, dict):
+            try:
+                obj = dict(obj)
+            except (TypeError, ValueError) as e:
+                exc = TypeError(f'{cls.__name__} expected dict not {type(obj).__name__}')
+                raise ValidationError([ErrorWrapper(exc, loc=ROOT_KEY)], cls) from e
         return cls(**obj)
 
     @classmethod


Test patch:
diff --git a/tests/test_main.py b/tests/test_main.py
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -900,10 +900,34 @@ class MyModel(BaseModel):
 
 
 def test_parse_root_as_mapping():
-    with pytest.raises(TypeError, match='custom root type cannot allow mapping'):
+    class MyModel(BaseModel):
+        __root__: Mapping[str, str]
 
-        class MyModel(BaseModel):
-            __root__: Mapping[str, str]
+    assert MyModel.parse_obj({1: 2}).__root__ == {'1': '2'}
+
+    with pytest.raises(ValidationError) as exc_info:
+        MyModel.parse_obj({'__root__': {'1': '2'}})
+    assert exc_info.value.errors() == [
+        {'loc': ('__root__', '__root__'), 'msg': 'str type expected', 'type': 'type_error.str'}
+    ]
+
+
+def test_parse_obj_non_mapping_root():
+    class MyModel(BaseModel):
+        __root__: List[str]
+
+    assert MyModel.parse_obj(['a']).__root__ == ['a']
+    assert MyModel.parse_obj({'__root__': ['a']}).__root__ == ['a']
+    with pytest.raises(ValidationError) as exc_info:
+        MyModel.parse_obj({'__not_root__': ['a']})
+    assert exc_info.value.errors() == [
+        {'loc': ('__root__',), 'msg': 'value is not a valid list', 'type': 'type_error.list'}
+    ]
+    with pytest.raises(ValidationError):
+        MyModel.parse_obj({'__root__': ['a'], 'other': 1})
+    assert exc_info.value.errors() == [
+        {'loc': ('__root__',), 'msg': 'value is not a valid list', 'type': 'type_error.list'}
+    ]
 
 
 def test_untouched_types():