3

I have data with a nested dict containing IDs as keys and data objects as values. I am trying to convert this into a pydantic model.

Sometimes there is a need to access a nested value if it exists. However, making an if check every time when accessing a nested object is very verbose. E.g.:

from typing import Dict
from pydantic import BaseModel

class EventModel(BaseModel):
    eventId: str
    eventName: str

class UserModel(BaseModel):
    userId: str
    events: Dict[str, EventModel]

user_dict = {
    'userId': 'abc',
    'events': {
        'xyz': {'eventId': 'xyz', 'eventName': 'User log in'},
        '123': {'eventId': '123', 'eventName': 'User log out'},
    },
}
user = UserModel(**user_dict)
unknown_id = '987'

# Inconvenient access pattern if there are multiple nested ID maps on the way
if user.events.get(unknown_id):
    print(user.events[unknown_id].eventName)

# Compare to the more convenient dict access pattern
print(user_dict['events'].get(unknown_id, {}).get('eventName'))

If the object is deep, the access pattern with if statements become very ugly.

Is there a more convenient way to access nested values with ID mappings on the way using pydantic? Or is there a way to improve this pydantic model to be more scalable without changing the underlying data structure?

Alex Waygood
  • 6,304
  • 3
  • 24
  • 46
Peter
  • 41
  • 4

2 Answers2

3

For a simple translation of your "more convenient dict access pattern", you could try this, which uses the optional default parameter of the built-in getattr function:

getattr(user.events.get(unknown_id), 'eventName', None)

A more efficient solution, however, might be to add a method to UserModel that will return as soon as it is known that there is no event with that ID. The following suggested solution follows the "Easier to ask for forgiveness than permission" pattern.

from typing import Dict
from pydantic import BaseModel

class EventModel(BaseModel):
    eventId: str
    eventName: str

class UserModel(BaseModel):
    userId: str
    events: Dict[str, EventModel]

    def name_of_event_with_id(event_id, default=None):
        try:
            event = self.events[event_id]
        except KeyError:
            return default
        else:
            return event.eventName
Alex Waygood
  • 6,304
  • 3
  • 24
  • 46
1

I ended up augmenting the library itself to include many features from the python dict interface, which helps quite a bit.

from pydantic import BaseModel as PydanticBasemodel


class BaseModel(PydanticBasemodel):
    # Enable instance[field] syntax
    def __getitem__(self, field):
        return getattr(self, field)

    def __setitem__(self, field, new_value):
        return setattr(self, field, new_value)

    # Enable spreading like a dict: func(**model)
    def keys(self):
        return self.__fields__.keys()

    # Enable dict like get
    def get(self, field, default=None):
        return self[field] if field in self.keys() else default

    # Change default config
    class Config:
        validate_assignment = True
Peter
  • 41
  • 4