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]
    ...

Asked By: woody

||

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
Answered By: Kassym Dorsel

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

Answered By: woody

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):
    ...
Answered By: efirvida