azo91
azo91

Reputation: 229

How to map values from nested dict to Pydantic Model?

I am trying to map a value from a nested dict/json to my Pydantic model. For me, this works well when my json/dict has a flat structure. However, I am struggling to map values from a nested structure to my Pydantic Model.

Lets assume I have a json/dict in the following format:

d = {
    "p_id": 1,
    "billing": {
        "first_name": "test"
    }
}

In addition, I have a Pydantic model with two attributes:

class Order(BaseModel):
    p_id: int
    pre_name: str

How can I map the value from the key first_nameto my Pydantic attribute pre_name?

Is there an easy way instead of using a root_validator to parse the given structure to my flat pydantic Model?

Upvotes: 13

Views: 17333

Answers (5)

R.K
R.K

Reputation: 1839

For my future self who lands here, a better solution is to use parse_obj of pydantic v2. You can use MyModel.parse_obj(my_dict) to generate a model from a dictionary. (documentation)

Example:

from pydantic import BaseModel, parse_obj_as
from typing import List

class TimeSlot(BaseModel):
    from_: str
    to: str

class DayActiveHours(BaseModel):
    days: List[str]
    time_slots: List[TimeSlot]

# Example nested dictionary
nested_data = {
    "days": ["Monday", "Tuesday"],
    "time_slots": [
        {"from_": "08:00", "to": "12:00"},
        {"from_": "13:00", "to": "17:00"}
    ]
}

# Parse the nested dictionary into Pydantic models
active_hours = DayActiveHours.parse_obj(nested_data)

# Now, you can access the parsed values using the Pydantic model attributes
print(active_hours.days)  # Output: ["Monday", "Tuesday"]
print(active_hours.time_slots)  # Output: [TimeSlot(from_='08:00', to='12:00'), TimeSlot(from_='13:00', to='17:00')]

Refer the solution here (pydantic v2) : How to parse list of models with Pydantic

Upvotes: -1

Eric Hansen
Eric Hansen

Reputation: 375

You could inherit from this custom class instead of BaseModel (see below).

from collections.abc import MutableMapping
from typing import Any, Iterator

from pydantic import BaseModel


class BaseModelDict(BaseModel, MutableMapping):
    """Goodness of BaseModel and acts like a dictionary."""

    def __contains__(self, x: str) -> bool:
        return True if x in self.__dict__.keys() else False

    def __delitem__(self, x: str) -> None:
        del self.__dict__[x]

    def __getitem__(self, x: str) -> Any:
        return self.__dict__[x]

    def __iter__(self) -> Iterator:
        return iter(self.__dict__)

    def __json__(self) -> dict:
        return self.__dict__

    def __len__(self) -> int:
        return len(self.__dict__)

    def __setitem__(self, key: str, value: Any) -> None:
        self.__dict__[key] = value

Upvotes: 0

Jorangutang
Jorangutang

Reputation: 430

You can nest pydantic models by making your lower levels another pydantic model. See as follows:

class MtnPayer(BaseModel):
  partyIdType: str
  partyId: str

class MtnPayment(BaseModel):
  financialTransactionId: str
  externalId: str
  amount: str
  currency: str
  payer: MtnPayer
  payerMessage: str
  payeeNote: str
  status: str
  reason: str

See the payer item in the second model

Upvotes: 0

funnydman
funnydman

Reputation: 11386

There's a package PyMapMe for mapping models, it supports nested models as well as helping functions and context, for example:

from typing import Any

from pydantic import BaseModel, Field
from pymapme.models.mapping import MappingModel


class Person(BaseModel):
    name: str
    surname: str


class Profile(BaseModel):
    nickname: str
    person: Person


class User(MappingModel):
    nickname: str = Field(source='nickname')
    first_name: str = Field(source='person.name')
    surname: str = Field(source='person.surname')
    full_name: str = Field(source_func='_get_full_name')

    @staticmethod
    def _get_full_name(model: Profile, default: Any):
        return model.person.name + ' ' + model.person.surname


profile = Profile(nickname='baobab', person=Person(name='John', surname='Smith'))
user = User.build_from_model(profile)
print(user.dict())  # {'nickname': 'baobab', 'first_name': 'John', 'surname': 'Smith', 'full_name': 'John Smith'}

Or for your example, it would look like:

d = {
    "p_id": 1,
    "billing": {
        "first_name": "test"
    }
}


class Billing(BaseModel):
    first_name: str


class Data(BaseModel):
    p_id: int
    billing: Billing


class Order(MappingModel):
    p_id: int
    pre_name: str = Field(source='billing.first_name')


order = Order.build_from_model(Data(**d))
print(order.dict())

Note: I'm the author, pull requests and any suggestions are welcome!

Upvotes: 3

alex_noname
alex_noname

Reputation: 32283

You can customize __init__ of your model class:

from pydantic import BaseModel

d = {
    "p_id": 1,
    "billing": {
        "first_name": "test"
    }
}


class Order(BaseModel):
    p_id: int
    pre_name: str

    def __init__(self, **kwargs):
        kwargs["pre_name"] = kwargs["billing"]["first_name"]
        super().__init__(**kwargs)


print(Order.parse_obj(d))  # p_id=1 pre_name='test'

Upvotes: 16

Related Questions