10

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

ch271828n
  • 15,854
  • 5
  • 53
  • 88
woody
  • 422
  • 1
  • 4
  • 12

3 Answers3

5

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=(), **kwargs):
        """
        allow some fields of subclass turn into optional
        """
        super().__init_subclass__(**kwargs)
        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
Arne
  • 17,706
  • 5
  • 83
  • 99
woody
  • 422
  • 1
  • 4
  • 12
4

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
Kassym Dorsel
  • 4,773
  • 1
  • 25
  • 52
  • Yes, it works. but I don't want to define the optional fields twice ( in fact, there are lots of fields). Can I change the fileds to optional by batch or some easy way? Thank you. – woody May 28 '20 at 03:18
  • By batch you'd still have to specify which fields are going to be optional, unless you want to make all fields optional? I've also edited to add another idea. – Kassym Dorsel May 28 '20 at 17:33
  • I have a new idea and post in my answer. – woody Jun 02 '20 at 12:20
  • This works well if you just want to add some fields without changing the types of the existing fields. Then there will be no duplication, and no use of dunder methods. – NeilG Aug 04 '23 at 06:35
1

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):
    ...
efirvida
  • 4,592
  • 3
  • 42
  • 68