Complex nested extraction
Real-world data is nested. A resume has personal info, a list of work experiences (each with company, title, dates), a list of education entries, and a list of skills. Pydantic handles this beautifully with nested models.
Nested model hierarchy
How Pydantic models nest to represent complex data like resumes
Pydantic supports arbitrary nesting depth. An Order can contain Products, each Product can contain Ingredients, each Ingredient can contain Allergens. However, the deeper you nest, the more specific your prompt needs to be so the AI knows the exact structure. Two to three levels deep is common in practice.
class WorkExperience(BaseModel):
company: str
title: str
start_date: str
end_date: str
description: Optional[str] = None
class Education(BaseModel):
school: str
degree: str
year: str
class Resume(BaseModel):
name: str
email: str
phone: Optional[str] = None
work_experience: list[WorkExperience]
education: list[Education]
skills: list[str]Nested Pydantic models: Resume contains lists of WorkExperience and Education objects. Each is independently validated.
list[WorkExperience] is modern Python syntax (3.9+) for type hints. It means "a list containing WorkExperience objects." Older code uses List[WorkExperience] from the typing module, which means the same thing.
resume_text = """
John Smith | [email protected] | (555) 987-6543
EXPERIENCE
Senior Developer at TechCorp (2020-Present)
- Led team of 5, built microservices platform
Junior Developer at StartupXYZ (2018-2020)
- Full-stack web development with React
EDUCATION
BS Computer Science, MIT, 2018
SKILLS: Python, JavaScript, React, AWS, Docker
"""
prompt = f"Extract resume as JSON: {resume_text}"
data = extract_json(prompt)
resume = Resume(**data)
print(f"Name: {resume.name}")
print(f"Skills: {', '.join(resume.skills)}")
print(f"Jobs: {len(resume.work_experience)}")The AI parses free-form resume text into a fully structured, validated Resume object with nested lists.
Use flat models when your data is a simple list of fields, like extracting a name and email. Use nested models when data has repeating groups, like multiple jobs on a resume or multiple items on an invoice. If you find yourself wanting to name fields job1_title, job2_title, that is a sign you need a nested list instead.
class InvoiceItem(BaseModel):
description: str
quantity: int
unit_price: float
total: float
class Invoice(BaseModel):
invoice_number: str
date: str
vendor: str
items: list[InvoiceItem]
subtotal: float
tax: float
total: float
# Extract and validate
invoice_data = extract_json(prompt)
invoice = Invoice(**invoice_data)
# Always verify totals!
verified_subtotal = sum(i.total for i in invoice.items)
print(f"Subtotal matches: {abs(verified_subtotal - invoice.subtotal) < 0.01}")Invoice processing: extract line items with quantities and prices, then verify all the math independently.
The same nested extraction pattern works for emails. Here we extract action items, each with a task, deadline, priority, and assignee, all validated by Pydantic.
class ActionItem(BaseModel):
task: str
deadline: str
priority: str
assigned_to: str
class EmailAnalysis(BaseModel):
subject: str
sender: str
action_items: list[ActionItem]
# Extract action items from a long email thread
email_data = extract_json(prompt)
analysis = EmailAnalysis(**email_data)
for item in analysis.action_items:
print(f"[{item.priority}] {item.task} -> {item.assigned_to} by {item.deadline}")Extract actionable tasks from emails with deadlines, priorities, and assignments, all validated by Pydantic.
AI prompt: Try it with AI
Loading practiceโฆ
Ordering exercise: Structured output pipeline steps
Loading practiceโฆ
Checkpoint: Structured outputs concepts
Loading practiceโฆ
You can now extract complex nested data from any text. Next, we will review best practices and put everything together in a final structured outputs challenge.