mirror of
https://github.com/kennethreitz/pydantic.git
synced 2026-06-05 23:00:18 +00:00
Add support for mapping types as custom root (#958)
* Add support for mapping types as custom root * Incorporate feedback * Add changes * Incorporate feedback * Add docs and tests * Fix linting issue * Incorporate more feedback * Add more specific match
This commit is contained in:
@@ -0,0 +1 @@
|
||||
Add support for mapping types for custom root models
|
||||
@@ -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)
|
||||
+22
-4
@@ -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
|
||||
|
||||
+10
-11
@@ -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 @@ class BaseModel(metaclass=ModelMetaclass):
|
||||
|
||||
@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
|
||||
|
||||
+27
-3
@@ -900,10 +900,34 @@ def test_root_undefined_failed():
|
||||
|
||||
|
||||
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():
|
||||
|
||||
Reference in New Issue
Block a user