11

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?

funnydman
  • 9,083
  • 4
  • 40
  • 55
azo91
  • 209
  • 1
  • 6
  • 15

4 Answers4

14

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'
alex_noname
  • 26,459
  • 5
  • 69
  • 86
2

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!

funnydman
  • 9,083
  • 4
  • 40
  • 55
0

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

Jorangutang
  • 170
  • 1
  • 7
0

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
Eric Hansen
  • 336
  • 2
  • 12