Pydantic: Modify the optional status of all fields in a model

Created on 23 Oct 2020  路  4Comments  路  Source: samuelcolvin/pydantic

Is there an elegant way to define a model with all fields optional, and then to define a second model which is identical but with all fields required? Or, alternatively, to modify the required/optional status of all fields on the first model?

An example:

class PersonalInformation(BaseModel):
    title: Optional[str] 
    forename: Optional[str] 
    surname: Optional[str] 

When doing a PATCH update (via FastAPI) on the DB data associated with this model, I want all the fields to be optional, so that the API user can supply only the fields they want to update. But later, in business logic, I want to be sure that all the fields are present. Specifying all the field names and base types twice seems like overkill, though: is there a way to do this without specifying two separate models in their entirety (as below)?

class PersonalInformationUpdate(BaseModel):
    title: Optional[str] 
    forename: Optional[str] 
    surname: Optional[str] 

class PersonalInformation(BaseModel):
    title: str
    forename:  str
    surname: str

The motivation is that, for larger models with complex validation, separate models leads to a fair amount of code duplication and a higher possibility of errors creeping in.

question

Most helpful comment

Hello @garry-jeromson
You could go with something like this

from typing import Optional, Type

from pydantic import BaseModel, create_model


class PersonalInformation(BaseModel):
    title: Optional[str]
    forename: Optional[str]
    surname: Optional[str]


def to_required(model: Type[BaseModel]) -> Type[BaseModel]:
    required_fields = {
        field_name: (model_field.type_, ...)
        for field_name, model_field in model.__fields__.items()
    }
    return create_model(f'Required{model.__name__}', **required_fields)

PersonalInformation()
# OK

RequiredPersonalInformation = to_required(PersonalInformation)
RequiredPersonalInformation()
# title
#   field required (type=value_error.missing)
# forename
#   field required (type=value_error.missing)
# surname
#   field required (type=value_error.missing)


class MandatoryPersonalInformation(BaseModel):
    title: str
    forename: str
    surname: str


def to_optional(model: Type[BaseModel]) -> Type[BaseModel]:
    optional_fields = {
        field_name: (model_field.type_, None)
        for field_name, model_field in model.__fields__.items()
    }
    return create_model(f'Optional{model.__name__}', **optional_fields)

MandatoryPersonalInformation()
# title
#   field required (type=value_error.missing)
# forename
#   field required (type=value_error.missing)
# surname
#   field required (type=value_error.missing)

OptionalPersonalInformation = to_optional(MandatoryPersonalInformation)
OptionalPersonalInformation()
# OK

You will need to make some tweaks if you have more complex fields.
Hope it helps !

All 4 comments

Hello @garry-jeromson
You could go with something like this

from typing import Optional, Type

from pydantic import BaseModel, create_model


class PersonalInformation(BaseModel):
    title: Optional[str]
    forename: Optional[str]
    surname: Optional[str]


def to_required(model: Type[BaseModel]) -> Type[BaseModel]:
    required_fields = {
        field_name: (model_field.type_, ...)
        for field_name, model_field in model.__fields__.items()
    }
    return create_model(f'Required{model.__name__}', **required_fields)

PersonalInformation()
# OK

RequiredPersonalInformation = to_required(PersonalInformation)
RequiredPersonalInformation()
# title
#   field required (type=value_error.missing)
# forename
#   field required (type=value_error.missing)
# surname
#   field required (type=value_error.missing)


class MandatoryPersonalInformation(BaseModel):
    title: str
    forename: str
    surname: str


def to_optional(model: Type[BaseModel]) -> Type[BaseModel]:
    optional_fields = {
        field_name: (model_field.type_, None)
        for field_name, model_field in model.__fields__.items()
    }
    return create_model(f'Optional{model.__name__}', **optional_fields)

MandatoryPersonalInformation()
# title
#   field required (type=value_error.missing)
# forename
#   field required (type=value_error.missing)
# surname
#   field required (type=value_error.missing)

OptionalPersonalInformation = to_optional(MandatoryPersonalInformation)
OptionalPersonalInformation()
# OK

You will need to make some tweaks if you have more complex fields.
Hope it helps !

A gentleman and a scholar. Thanks!

Sorry for the noise on a closed issue, but it seems preferable to opening another issue on a topic that already exists in multiple forms.

@PrettyWood You have suggested this approach more than once, is there a reason to do it your way rather than the following?

class MandatoryPersonalInformation(BaseModel):
    title: str
    forename: str
    surname: str

def to_optional(model: Type[BaseModel]) -> Type[BaseModel]:

    baseclass: Any = model

    class Partial(baseclass):
        pass

    for field in Partial.__fields__.values():
        field.required = False

    Partial.__name__ = f"Partial{model.__name__}"

    return Partial

OptionalPersonalInformation = to_optional(MandatoryPersonalInformation)

OptionalPersonalInformation()

With your solution I had trouble with mypy when using the resulting model.

Hi @dashavoo,
It's just the first solution that came to my mind but of course your solution works as well.
I didn't even check how it would work out with mypy ;)
Thanks for sharing

Was this page helpful?
0 / 5 - 0 ratings

Related issues

dconathan picture dconathan  路  3Comments

samuelcolvin picture samuelcolvin  路  3Comments

AlbertMukhammadiev picture AlbertMukhammadiev  路  3Comments

engstrom picture engstrom  路  3Comments

cdeil picture cdeil  路  3Comments