How to extend a Pydantic object and change some fields' type?
Question:
There are two similar pydantic object like that. The only difference is some fields are optionally.
How can I just define the fields in one object and extend into another one?
class ProjectCreateObject(BaseModel):
project_id: str
project_name: str
project_type: ProjectTypeEnum
depot: str
system: str
...
class ProjectPatchObject(ProjectCreateObject):
project_id: str
project_name: Optional[str]
project_type: Optional[ProjectTypeEnum]
depot: Optional[str]
system: Optional[str]
...
Answers:
You’ve pretty much answered it yourself. Unless there’s something more to the question.
from typing import Optional
from pydantic import BaseModel
class ProjectCreateObject(BaseModel):
project_id: str
project_name: str
project_type: str
depot: str
system: str
class ProjectPatchObject(ProjectCreateObject):
project_name: Optional[str]
project_type: Optional[str]
depot: Optional[str]
system: Optional[str]
if __name__ == "__main__":
p = ProjectCreateObject(
project_id="id",
project_name="name",
project_type="type",
depot="depot",
system="system",
)
print(p)
c = ProjectPatchObject(project_id="id", depot="newdepot")
print(c)
Running this gives:
project_id='id' project_name='name' project_type='type' depot='depot' system='system'
project_id='id' project_name=None project_type=None depot='newdepot' system=None
Another way to look at it is to define the base as optional and then create a validator to check when all required:
from pydantic import BaseModel, root_validator, MissingError
class ProjectPatchObject(BaseModel):
project_id: str
project_name: Optional[str]
project_type: Optional[str]
depot: Optional[str]
system: Optional[str]
class ProjectCreateObject(ProjectPatchObject):
@root_validator
def check(cls, values):
for k, v in values.items():
if v is None:
raise MissingError()
return values
I find a good and easy way by __init__subclass__
.
The docs also can be generated successfully.
class ProjectCreateObject(BaseModel):
project_id: str
project_name: str
project_type: ProjectTypeEnum
depot: str
system: str
...
def __init_subclass__(cls, optional_fields=None, **kwargs):
"""
allow some fields of subclass turn into optional
"""
super().__init_subclass__(**kwargs)
if optional_fields:
for field in optional_fields:
cls.__fields__[field].outer_type_ = Optional
cls.__fields__[field].required = False
_patch_fields = ProjectCreateObject.__fields__.keys() - {'project_id'}
class ProjectPatchObject(ProjectCreateObject, optional_fields=_patch_fields):
pass
Or use metaclass like in this thread: Make every fields as optional with Pydantic
class AllOptional(pydantic.main.ModelMetaclass):
def __new__(self, name, bases, namespaces, **kwargs):
annotations = namespaces.get('__annotations__', {})
for base in bases:
annotations.update(base.__annotations__)
for field in annotations:
if not field.startswith('__') and field != 'project_id':
annotations[field] = Optional[annotations[field]]
namespaces['__annotations__'] = annotations
return super().__new__(self, name, bases, namespaces, **kwargs)
And in your example…
class ProjectPatchObject(ProjectCreateObject, metaclass=AllOptional):
...
There are two similar pydantic object like that. The only difference is some fields are optionally.
How can I just define the fields in one object and extend into another one?
class ProjectCreateObject(BaseModel):
project_id: str
project_name: str
project_type: ProjectTypeEnum
depot: str
system: str
...
class ProjectPatchObject(ProjectCreateObject):
project_id: str
project_name: Optional[str]
project_type: Optional[ProjectTypeEnum]
depot: Optional[str]
system: Optional[str]
...
You’ve pretty much answered it yourself. Unless there’s something more to the question.
from typing import Optional
from pydantic import BaseModel
class ProjectCreateObject(BaseModel):
project_id: str
project_name: str
project_type: str
depot: str
system: str
class ProjectPatchObject(ProjectCreateObject):
project_name: Optional[str]
project_type: Optional[str]
depot: Optional[str]
system: Optional[str]
if __name__ == "__main__":
p = ProjectCreateObject(
project_id="id",
project_name="name",
project_type="type",
depot="depot",
system="system",
)
print(p)
c = ProjectPatchObject(project_id="id", depot="newdepot")
print(c)
Running this gives:
project_id='id' project_name='name' project_type='type' depot='depot' system='system'
project_id='id' project_name=None project_type=None depot='newdepot' system=None
Another way to look at it is to define the base as optional and then create a validator to check when all required:
from pydantic import BaseModel, root_validator, MissingError
class ProjectPatchObject(BaseModel):
project_id: str
project_name: Optional[str]
project_type: Optional[str]
depot: Optional[str]
system: Optional[str]
class ProjectCreateObject(ProjectPatchObject):
@root_validator
def check(cls, values):
for k, v in values.items():
if v is None:
raise MissingError()
return values
I find a good and easy way by __init__subclass__
.
The docs also can be generated successfully.
class ProjectCreateObject(BaseModel):
project_id: str
project_name: str
project_type: ProjectTypeEnum
depot: str
system: str
...
def __init_subclass__(cls, optional_fields=None, **kwargs):
"""
allow some fields of subclass turn into optional
"""
super().__init_subclass__(**kwargs)
if optional_fields:
for field in optional_fields:
cls.__fields__[field].outer_type_ = Optional
cls.__fields__[field].required = False
_patch_fields = ProjectCreateObject.__fields__.keys() - {'project_id'}
class ProjectPatchObject(ProjectCreateObject, optional_fields=_patch_fields):
pass
Or use metaclass like in this thread: Make every fields as optional with Pydantic
class AllOptional(pydantic.main.ModelMetaclass):
def __new__(self, name, bases, namespaces, **kwargs):
annotations = namespaces.get('__annotations__', {})
for base in bases:
annotations.update(base.__annotations__)
for field in annotations:
if not field.startswith('__') and field != 'project_id':
annotations[field] = Optional[annotations[field]]
namespaces['__annotations__'] = annotations
return super().__new__(self, name, bases, namespaces, **kwargs)
And in your example…
class ProjectPatchObject(ProjectCreateObject, metaclass=AllOptional):
...