salius
salius

Reputation: 1088

Python pydantic, make every field of ancestor are Optional

I have 2 classes:

class UserCreate(BaseModel):
    avatar: HttpUrl = Field(..., description="Avatar", example="https://picsum.photos/200")
    name: str = Field(..., max_length=20, description="A single word", example='Ivan')
    birthdate: datetime_date = Field(..., description="Two digits", example='1980-1-1')
    comment: Optional[str] = Field(..., max_length=512, description="lorem ipsum about a user", example='blah blah')

and I want to create a UserUpdate class that will inherit every field from the parent class and make it Optional.

a Result class must look like:

class UserUpdate(BaseModel):
    avatar: typing.Optional[HttpUrl] = Field(..., description="Avatar", example="https://picsum.photos/200")
    name: typing.Optional[str] = Field(..., max_length=20, description="A single word", example='Ivan')
    birthdate: typing.Optional[datetime_date] = Field(..., description="Two digits", example='1980-1-1')
    comment: typing.Optional[str] = Field(..., max_length=512, description="lorem ipsum about a user", example='blah blah')

But obviously, I want to make it automatically, like:

class UserUpdate(UserCreate):
    def foo(fields_from_user_create):
        for fields in fields_from_user_create:
            field = typing.Optional(field)

Upvotes: 3

Views: 4935

Answers (3)

winwin
winwin

Reputation: 1797

Based on my answer from here:

from typing import Optional, Type, Any, Tuple
from copy import deepcopy

from pydantic import BaseModel, create_model
from pydantic.fields import FieldInfo


def partial_model(model: Type[BaseModel]):
    def make_field_optional(field: FieldInfo, default: Any = None) -> Tuple[Any, FieldInfo]:
        new = deepcopy(field)
        new.default = default
        new.annotation = Optional[field.annotation]  # type: ignore
        return new.annotation, new
    return create_model(
        f'Partial{model.__name__}',
        __base__=model,
        __module__=model.__module__,
        **{
            field_name: make_field_optional(field_info)
            for field_name, field_info in model.__fields__.items()
        }
    )

Usage:

@partial_model
class Model(BaseModel):
    i: int
    f: float
    s: str


Model(i=1)

Upvotes: 0

salius
salius

Reputation: 1088

A solution directly from PrettyWood

Here is a way of doing it

from copy import deepcopy
from typing import Optional, Type, TypeVar

from pydantic import BaseModel, create_model

BaseModelT = TypeVar('BaseModelT', bound=BaseModel)


def to_optional(model: Type[BaseModelT], name: Optional[str] = None) -> Type[BaseModelT]:
    """
    Create a new BaseModel with the exact same fields as `model`
    but making them all optional
    """
    field_definitions = {}
    for name, field in model.__fields__.items():
        optional_field_info = deepcopy(field.field_info)
        # Do not change default value of fields that are already optional
        if optional_field_info.default is ...:
            optional_field_info.default = None
        field_type = model.__annotations__.get(name, field.outer_type_)
        field_definitions[name] = (field_type, optional_field_info)

return create_model(name or f'Optional{model.__name__}', **field_definitions)  # type: ignore[arg-type]

Hope it helps ;)

Original answer

Upvotes: 2

Andreas Profous
Andreas Profous

Reputation: 1512

I am using the following approach (credit goes to Aron Podrigal):

import inspect   
from pydantic import BaseModel   


def optional(*fields):
    """Decorator function used to modify a pydantic model's fields to all be optional.
    Alternatively, you can  also pass the field names that should be made optional as arguments
    to the decorator.
    Taken from https://github.com/samuelcolvin/pydantic/issues/1223#issuecomment-775363074
    """   
    def dec(_cls):
        for field in fields:
            _cls.__fields__[field].required = False
        return _cls

    if fields and inspect.isclass(fields[0]) and issubclass(fields[0], BaseModel):
        cls = fields[0]
        fields = cls.__fields__
        return dec(cls)

    return dec

   

In your example you'd use it like this:

@optional
class UserUpdate(UserCreate):
    pass

Upvotes: 3

Related Questions