This concise, practical article is about nested models in Pydantic (version 2.4 and above).
Overview
In Pydantic, a nested model is a model that is defined as a field of another model. This allows you to define complex data structures with multiple levels of nesting. In general, the steps to define and use a nested model are as follows:
- Define the nested model as a separate Pydantic model class.
- Define the parent model class (or the container model class) with a field that uses the nested model class as its type.
- Use the parent model class to validate input data.
Transforming these steps into action often entails unforeseen complexities. Let’s take a look at some code examples to get a better and stronger understanding.
Basic Example
In this example, we define two Pydantic models: Item
and Order
. Item
is a simple model that represents an item with a name, price, and optional tax. Order
is a model that contains a list of Item
objects and a customer name.
from typing import List
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
tax: float = 0.0
class Order(BaseModel):
items: List[Item]
customer: str
Now, we create an order_data
dictionary that contains a list of two items and a customer name. We then create an Order
object by passing the order_data
dictionary to the Order
constructor. Finally, we print the order
object to verify that it was created correctly:
from typing import List
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
tax: float = 0.0
class Order(BaseModel):
items: List[Item]
customer: str
order_data = {
"items": [
{"name": "item1", "price": 10.0},
{"name": "item2", "price": 20.0, "tax": 2.0},
],
"customer": "[email protected]",
}
order = Order(**order_data)
print(order)
Output:
items=[Item(name='item1', price=10.0, tax=0.0), Item(name='item2', price=20.0, tax=2.0)] customer='[email protected]'
This example might seem too simple and can make you get bored. Don’t leave your keyboard, the next one will warm you up.
Advanced Example
In this example, you’ll see both a parent model and a grandparent model (deeply nested model). Let’s read the code first (the explanation will come later):
from typing import List
from pydantic import BaseModel
# This is a child model that will be nested inside "Contact"
class Address(BaseModel):
street: str
city: str
state: str
zip_code: str
# This is a child model that will be nested inside "Contact"
class Phone(BaseModel):
number: str
type: str
# This is a child model that will be nested inside "Contact"
class Email(BaseModel):
address: str
type: str
# That is the parent model of the 3 child models above
# But it is also a child model of the "Company" model
class Contact(BaseModel):
first_name: str
last_name: str
age: int
address: Address
phones: List[Phone]
emails: List[Email]
# This is the parent model of the "Contact" model
class Company(BaseModel):
name: str
employees: List[Contact]
company_data = {
"name": "Acme Inc.",
"employees": [
{
"first_name": "John",
"last_name": "Doe",
"age": 30,
"address": {
"street": "123 Main St",
"city": "Anytown",
"state": "CA",
"zip_code": "12345",
},
"phones": [
{"number": "555-1234", "type": "home"},
{"number": "555-5678", "type": "work"},
],
"emails": [
{"address": "[email protected]", "type": "work"},
{"address": "[email protected]", "type": "personal"},
],
},
{
"first_name": "Jane",
"last_name": "Doe",
"age": 25,
"address": {
"street": "456 Elm St",
"city": "Anytown",
"state": "CA",
"zip_code": "12345",
},
"phones": [
{"number": "555-4321", "type": "home"},
{"number": "555-8765", "type": "work"},
],
"emails": [
{"address": "[email protected]", "type": "work"},
{"address": "[email protected]", "type": "personal"},
],
},
],
}
company = Company(**company_data)
print(company)
In this example, we define six Pydantic models: Address
, Phone
, Email
, Contact
, and Company
. Address
is the same as in the previous example. Phone
is a model that represents a phone number with a number and a type. Email
is a model that represents an email address with an address and a type. Contact
is a model that represents a person with a first name, last name, age, address, a list of phones, and a list of emails. Company
is the same as in the previous example.
We then create a company_data
dictionary that contains a company name and a list of two employees, each with their own address, phones, and emails. We create a Company
object by passing the company_data
dictionary to the Company
constructor. Finally, we print the company
object to verify that it was created correctly.
Here’s the output:
name='Acme Inc.' employees=[Contact(first_name='John', last_name='Doe', age=30, address=Address(street='123 Main St', city='Anytown', state='CA', zip_code='12345'), phones=[Phone(number='555-1234', type='home'), Phone(number='555-5678', type='work')], emails=[Email(address='[email protected]', type='work'), Email(address='[email protected]', type='personal')]), Contact(first_name='Jane', last_name='Doe', age=25, address=Address(street='456 Elm St', city='Anytown', state='CA', zip_code='12345'), phones=[Phone(number='555-4321', type='home'), Phone(number='555-8765', type='work')], emails=[Email(address='[email protected]', type='work'), Email(address='[email protected]', type='personal')])]
Conclusion
Using nested models in Pydantic can be beneficial for validating and parsing complex data structures that contain other models as fields. However, it also has some drawbacks, such as slower performance, more verbose code, and potential issues with serialization and deserialization. Therefore, one should weigh the pros and cons of using nested models depending on the use case and the data source.