Repository: pydantic/pydantic
Pull request: Add support for mapping types as custom root
## Change Summary

Modifies `parse_obj` and `MetaModel` to allow mapping types with a custom root.

I needed this to address some of the feedback on #934 

## Related issue number

Closes #908

## Checklist

* [x] Unit tests for the changes exist
* [x] Tests pass on CI and coverage remains at 100%
* [x] Documentation reflects the changes where applicable
* [x] `changes/<pull request or issue id>-<github username>.md` file added describing change
  (see [changes/README.md](https://github.com/samuelcolvin/pydantic/blob/master/changes/README.md) for details)


Problem statement:
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!

Hints from issue 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.)
Hello there,

i'm quite interested in this feature too. I'm getting a payload I don't control the schema. The payload is an object where the keys are all unknown (and of a various format). I wanted to use a mapping as a root type but not sure how to go about it until this has a chance to be reviewed/merged/released.

Cheers
Thanks for your interest, see the discussion on #958 as it happens I'm reviewing the last comment right now and working out what to do.

Commit message: Incorporate more feedback

Patch to review:
diff --git a/changes/958-dmontagu.md b/changes/958-dmontagu.md
new file mode 100644
index 00000000000..b5af62cccbf
--- /dev/null
+++ b/changes/958-dmontagu.md
@@ -0,0 +1 @@
+Add support for mapping types for custom root models 
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
index 00000000000..f5d3da2ea36
--- /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/docs/usage/models.md b/docs/usage/models.md
index ad4c753efea..63aa95dc79e 100644
--- a/docs/usage/models.md
+++ b/docs/usage/models.md
@@ -324,17 +324,35 @@ extending a base model with extra fields.
 
 ## Custom Root Types
 
-Pydantic models which do not represent a `dict` ("object" in JSON parlance) can have a custom
-root type defined via the `__root__` field. The root type can be of any type: list, float, int, etc.
+Pydantic models can be defined with a custom root type by declaring the `__root__` field. 
 
-The root type is defined via the type hint on the `__root__` field.
-The root value can be passed to model `__init__` via the `__root__` keyword argument or as
+The root type can be any type supported by pydantic, and is specified by the type hint on the `__root__` field.
+The root value can be passed to the model `__init__` via the `__root__` keyword argument, or as
 the first and only argument to `parse_obj`.
 
 ```py
 {!.tmp_examples/models_custom_root_field.py!}
 ```
 
+If you call the `parse_obj` method for a model with a custom root type with a *dict* as the first argument,
+the following logic is used:
+
+* If the custom root type is a mapping type (eg., `Dict` or `Mapping`),
+  the argument itself is always validated against the custom root type.
+* For other custom root types, if the dict has precisely one key with the value `__root__`,
+  the corresponding value will be validated against the custom root type.
+* Otherwise, the dict itself is validated against the custom root type.    
+
+This is demonstrated in the following example:
+
+```py
+{!.tmp_examples/models_custom_root_field_parse_obj.py!}
+```
+
+!!! warning
+    Calling the `parse_obj` method on a dict with the single key `"__root__"` for non-mapping custom root types
+    is currently supported for backwards compatibility, but is not recommended and may be dropped in a future version.
+
 ## Faux Immutability
 
 Models can be configured to be immutable via `allow_mutation = False`. When this is set, attempting to change the
diff --git a/pydantic/main.py b/pydantic/main.py
index ecfb92f8811..95fe04eb70a 100644
--- 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
diff --git a/tests/test_main.py b/tests/test_main.py
index 29c7f948c4f..d0aa42f6da5 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -900,10 +900,28 @@ 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, match='...'):
+        MyModel.parse_obj({'__not_root__': ['a']})
+    with pytest.raises(ValidationError, match='...'):
+        MyModel.parse_obj({'__root__': ['a'], 'other': 1})
 
 
 def test_untouched_types():


Code review comments:
diff_hunk: @@ -918,6 +918,10 @@ class MyModel(BaseModel):
 
     assert MyModel.parse_obj(['a']).__root__ == ['a']
     assert MyModel.parse_obj({'__root__': ['a']}).__root__ == ['a']
+    with pytest.raises(ValidationError, match='...'):
+        MyModel.parse_obj({'__not_root__': ['a']})
+    with pytest.raises(ValidationError, match='...'):
path: tests/test_main.py
text: @user1:
We need something in the `match`! :smile: (I was being lazy).

@author:
Haha okay hold on

@author:
Should be better now