Using Nested Models in Pydantic (with Examples)

Updated: December 1, 2023 By: Khue Post a comment

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:

  1. Define the nested model as a separate Pydantic model class.
  2. Define the parent model class (or the container model class) with a field that uses the nested model class as its type.
  3. 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 OrderItem 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: AddressPhoneEmailContact, and CompanyAddress 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.