Return Pydantic model as None if all fields are None

Question:

Let’s say I have a pydantic model with an optional field:

class MyModel(BaseModel):
    field_1: str | None

That I instantiate by passing a dict using parse_obj()

Now, I would like the pydantic object to be None if none of the field members were set.

Example:

data_a = {
    'field_1': 'value_1'
}
obj_a = MyModel.parse_obj(data_a)
print(type(obj_a))  # <class 'MyModel'>
  

data_b = {
    'foo': 'bar'
}
obj_b = MyModel.parse_obj(data_b)
print(type(obj_b))  # I would like this to be NoneType !

Of course I know I could check if the fields exist in the input data before making any instantiation, but I want to avoid that and make it in a more generic way (imagine like having many different models with different fields).

Asked By: sebaspla

||

Answers:

You could use all() and a list comprehension:

if all(val is None for val in dict(obj_b).values()):
    obj_b = None

Or, alternatively, if none of the fields will purposefully be set to None, you could check if any of the fields have been set:

if not obj_b.__fields_set__:
    obj_b = None

Both of these could be compressed:

# First
obj_b = None if all(val is None for val in dict(obj_b).values()) else obj_b
# Second
obj_b = obj_b if obj_b.__fields_set__ else None

Here’s a base class that does this automatically:

class NoneCheckModel(BaseModel):
    """Model with None checking"""
    @classmethod
    def parse_obj(*args, **kwargs):
        result = super().parse_obj(*args, **kwargs)
        return None if all(val is None for val in dict(result).values()) else result
Answered By: pigrammer

Unfortunately for you, pydantic’s root validation will either raise an error or instantiate the class. A different approach is required for your desired outcome. A solution here is to override the BaseClass‘s __new__ operator (which invokes the class to create a new instance) – to not instantiate if conditions aren’t met:

from typing import Optional
from pydantic import BaseModel

class MyBaseModel(BaseModel):
    """
    Pydantic baseclass with overloaded operator for
    instantiating new objects.
    """
    def __new__(cls, *args, **kwargs):
        """Be careful when using this method:
        https://docs.python.org/3/reference/datamodel.html#object.__new__
        """
        # If all args are none -> do nothing
        if all(v is None for v in args) and all(v is None for v in kwargs.values()):
            pass
        else:
            return super().__new__(cls)

class MyModel(MyBaseModel):
    field_1: Optional[str]
    field_2: Optional[str]


data_a = {'field_1': 'foo', 'field_2': None}
data_b = {'field_1': None, 'field_2': None}

obj_a = MyModel.parse_obj(data_a)
print(type(obj_a))  # <class 'MyModel'>

obj_b = MyModel.parse_obj(data_b)
print(type(obj_b))  # <class 'NoneType'>
Answered By: Yaakov Bressler
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.