Reputation: 229
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_name
to 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
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
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
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
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
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