New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
support custom root types #628
Conversation
Codecov Report
@@ Coverage Diff @@
## master #628 +/- ##
=====================================
Coverage 100% 100%
=====================================
Files 15 15
Lines 2634 2651 +17
Branches 518 524 +6
=====================================
+ Hits 2634 2651 +17 |
Otherwise the idea looks good, I'll review more once it's complete.
docs/index.rst
Outdated
@@ -466,6 +466,18 @@ Outputs: | |||
.. literalinclude:: examples/schema3.json | |||
|
|||
|
|||
Pydantic models can be set custom root types to support a model that does not have any field exclude ``__root__``. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you make this a separate section of the docs. This is not just about schema generation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. I have moved the content to around types sections.
pydantic/main.py
Outdated
@@ -186,7 +186,7 @@ def __new__(mcs, name, bases, namespace): | |||
for ann_name, ann_type in annotations.items(): | |||
if is_classvar(ann_type): | |||
class_vars.add(ann_name) | |||
elif not ann_name.startswith('_') and ann_name not in namespace: | |||
elif (not ann_name.startswith('_') or '__root__' == ann_name) and ann_name not in namespace: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we do this at least three times, can we have a function for it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have created is_valid_field function
for checking a field name.
pydantic/main.py
Outdated
_json_encoder: Callable[[Any], Any] = lambda x: x | ||
_schema_cache: 'DictAny' = {} | ||
|
||
Config = BaseConfig | ||
__slots__ = ('__values__', '__fields_set__') | ||
|
||
def __init__(self, **data: Any) -> None: | ||
def __init__(self, *__root__: Any, **data: Any) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Humm, I've very unwilling to change the signature of __init__
.
Would it be possible to accept __root__
directly to parse_obj
only?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have reverted the __init__
method to master.
And I add a __root__
argument in parse_obj
.
However, We can pass __root__
object to __init__
as a keyword argument.
Model(__root__='a')
# or
Model.parse_obj(__root__='a')
add a keyword argument of "__root__" in parse_obj fix documents create a method for cheking valid field name
pydantic/main.py
Outdated
@@ -233,6 +241,7 @@ class BaseModel(metaclass=MetaModel): | |||
__fields__: Dict[str, Field] = {} | |||
__validators__: Dict[str, AnyCallable] = {} | |||
__config__: Type[BaseConfig] = BaseConfig | |||
__root__: List[Any] = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought __root__
doesn't have to be a list?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, it's my mistake. I have changed List[Any]
to Any
.
pydantic/main.py
Outdated
@@ -211,6 +217,8 @@ def __new__(mcs, name, bases, namespace): | |||
config=config, | |||
) | |||
|
|||
if '__root__' in fields and len(fields) > 1: | |||
raise ValueError('__root__ cannot be mixed with other fields') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add _custom_root_type: bool
to new_namespace
below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have added it.
pydantic/main.py
Outdated
@@ -324,9 +334,17 @@ def json( | |||
) | |||
|
|||
@classmethod | |||
def parse_obj(cls: Type['Model'], obj: Mapping[Any, Any]) -> 'Model': | |||
def parse_obj( | |||
cls: Type['Model'], obj: Optional[Mapping[Any, Any]] = None, __root__: Optional[Any] = None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
simplify this to
def parse_obj(cls: Type['Model'], obj: Any) -> 'Model'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, I just changed it.
pydantic/main.py
Outdated
if not isinstance(obj, dict): | ||
try: | ||
if obj is None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
change this to:
if not instanstance(obj, dict):
if cls._custom_root_type:
obj = {'__root__': obj}
else:
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should also raise an error if the type of __root__
is a dict.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have changed the condition and added to raise an exception.
class MyModel(BaseModel): | ||
__root__: str | ||
|
||
m = MyModel(__root__='a') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please test with parse_obj
too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had made tests for parse_obj
in tests/tset_parse.py
Would you please check it?
https://github.com/samuelcolvin/pydantic/pull/628/files#diff-7b34c8bd665994bbdcf056c03622d4a8R45
Co-Authored-By: Samuel Colvin <samcolvin@gmail.com>
Co-Authored-By: Samuel Colvin <samcolvin@gmail.com>
sorry, reviewed this days ago and somehow forgot to press submit.
pydantic/main.py
Outdated
def parse_obj(cls: Type['Model'], obj: Any) -> 'Model': | ||
if isinstance(obj, dict): | ||
if cls._custom_root_type: | ||
raise TypeError('custom root type cannot allow dict') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this test should be moved to model creation, not validation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I have moved it to __new__
in MetaModel.
Also, I change the validation type from dict
to Mapping
. If it's wrong, then please tell me correct.
… custom_root_types
Co-Authored-By: Samuel Colvin <samcolvin@gmail.com>
No problem, Thank you for reviewing my code. |
Hey @koxudaxi @samuelcolvin. I have a question that is related to this PR. Given the following use case: from typing import List
from datetime import datetime
from pydantic import BaseModel
class LotteryResults(BaseModel):
__root__: List[int]
class Lottery(BaseModel):
when: datetime
numbers: LotteryResults
numbers = LotteryResults.parse_obj([10, 20, 30, 12 , 34]
lottery = Lottery(when=datetime.now(), numbers=numbers) Running that code as is, when I call the In [7]: lottery.json()
Out[7]: '{"when": "2020-05-21T14:09:14.652009", "numbers": {"__root__": [10, 20, 30, 12, 34]}}' Is it possible to have the In [7]: lottery.json()
Out[7]: '{"when": "2020-05-21T14:09:14.652009", "numbers": [10, 20, 30, 12, 34]}' By the way, Pydantic is awesome! |
I paste a workaround. Btw, I think you should create a new issue for this question. import json
from typing import Any
from typing import List
from datetime import datetime
from pydantic import BaseModel
from pydantic.class_validators import ROOT_KEY
def unpack_custom_root(value: Any):
if isinstance(value, dict):
if ROOT_KEY in value:
return value[ROOT_KEY]
return {k: unpack_custom_root(v) for k, v in value.items()}
return value
def custom_root_json_dumps(value, *, default):
return json.dumps(unpack_custom_root(value), default=default)
class LotteryResults(BaseModel):
__root__: List[int]
class Lottery(BaseModel):
when: datetime
numbers: LotteryResults
class Config:
json_dumps = custom_root_json_dumps
numbers = LotteryResults.parse_obj([10, 20, 30, 12, 34])
lottery = Lottery(when=datetime.now(), numbers=numbers)
print(lottery.json())
# {"when": "2020-05-22T00:32:10.811364", "numbers": [10, 20, 30, 12, 34]} |
Change Summary
support custom root types that support a model that does not have any field exclude
__root__
.example:
Related issue number
#507
Checklist
HISTORY.rst
has been updated#628
@koxudaxi