1

I have a pydantic model with many fields that are also pydantic models. I would like to make recursively all fields optional.

For example:

class MyModelE(BaseModel):
   f: float

class MyModelA(BaseModel):
   d: str
   e: MyModelE

class MyModel(BaseModel):
   a: MyModelA
   b: int

I would like to have a metaclass or method to make all fields of MyModel optional, including fields a, b, d, e, and f.

The solutions described in this previously asked question only make fields that are non-pydantic models optional.

Make every fields as optional with Pydantic

morfys
  • 2,195
  • 3
  • 28
  • 35
  • _"The solutions [...] only make fields that are non-pydantic models optional."_ Are you sure? The meta class solution you linked to works fine for me, regardless of the field type, including fields that are themselves Pydantic models. – Daniil Fajnberg Apr 18 '23 at 16:08
  • Also, pretty sure you have a typo in your code: `a = MyModelA` probably needs to be `a: MyModelA`. And without `__future__.annotations` those forward referencing type annotations are not going to work. – Daniil Fajnberg Apr 18 '23 at 16:11
  • Oh, and just noticed `MyModelA` and `MyModelE` aren't even Pydantic models here at all... I'm voting to close this. – Daniil Fajnberg Apr 18 '23 at 17:59

1 Answers1

0

This is a solution for pydantic v2.

It naturally uses depth first search down the hierarchy of composition and then with the field info found, it builds models with required=False bottom-up towards the top.

Also, it does not use any low level hack that depends on the internal implementation of pydantic objects, many of which are changed or not available in v2.

from pydantic import BaseModel, create_model

class MyModelE(BaseModel):
       f: float

class MyModelA(BaseModel):
   d: str
   e: MyModelE

class MyModel(BaseModel):
   a: MyModelA
   b: int

def is_pydantic_model(obj):
    try:
        return issubclass(obj, BaseModel)
    except TypeError:
        return False

def show_hierarchy(Model: BaseModel, indent: int=0):
    for k, v in Model.model_fields.items():
        print(f'{" "*indent}{Model.__name__}.{k}: '
              f'type={getattr(v.annotation, "__name__", v.annotation)}, '
              f'required={v.is_required()}')
        if is_pydantic_model(v.annotation):
            show_hierarchy(v.annotation, indent+2)

def unset_required(Model: BaseModel, name: str=None) -> BaseModel:
    fields = {}
    for k, v in Model.model_fields.items():
        if is_pydantic_model(v.annotation):
            fields[k] = (unset_required(v.annotation), None) 
        else:
            fields[k] = (v.annotation, None) 
    return create_model(name if name is not None else Model.__name__, **fields)

show_hierarchy(MyModel)
print('')
UpdateModel = unset_required(MyModel, name='UpdateModel')
show_hierarchy(UpdateModel)

This results in

MyModel.a: type=MyModelA, required=True
  MyModelA.d: type=str, required=True
  MyModelA.e: type=MyModelE, required=True
    MyModelE.f: type=float, required=True
MyModel.b: type=int, required=True

UpdateModel.a: type=MyModelA, required=False
  MyModelA.d: type=str, required=False
  MyModelA.e: type=MyModelE, required=False
    MyModelE.f: type=float, required=False
UpdateModel.b: type=int, required=False

It can handle a model where the same sub-model is used multiple times at different levels as well.

class MyModelE(BaseModel):
   f: float

class MyModelA(BaseModel):
   d: str
   e: MyModelE  # MyModelE

class MyModel(BaseModel):
   a: MyModelA
   b: MyModelE  # MyModelE

MyModel.a: type=MyModelA, required=True
  MyModelA.d: type=str, required=True
  MyModelA.e: type=MyModelE, required=True
    MyModelE.f: type=float, required=True # MyModelE
MyModel.b: type=MyModelE, required=True
  MyModelE.f: type=float, required=True   # MyModelE

UpdateModel.a: type=MyModelA, required=False
  MyModelA.d: type=str, required=False
  MyModelA.e: type=MyModelE, required=False
    MyModelE.f: type=float, required=False # MyModelE
UpdateModel.b: type=MyModelE, required=False
  MyModelE.f: type=float, required=False   # MyModelE
jung rhew
  • 800
  • 6
  • 9