Mario Brusarosco

pydantic objects x dicts

In the ground since Mon Jan 15 2024

Last watered inMon Jan 15 2024

Related Topics

Pydantic Objects vs Dictionaries

Understanding the core issue when working with Pydantic objects and dictionaries in FastAPI applications.

The Core Issue

When working with Pydantic models across different layers of your application, you'll encounter scenarios where you need to pass data between different Pydantic models. The key issue is that Pydantic is strict about type validation - even if two models have identical fields, they're considered different types.

Common Scenarios

Scenario 1: Direct Assignment (✅ Works)

This works - creating a Pydantic model from a dictionary:

1data = {"months": 10, "total": "R$ 960,60"}
2option = CreditCardInstallmentOption(**data) # ✅ Success

Scenario 2: Nested Pydantic Objects (❌ Fails)

This fails - trying to assign a Pydantic object where dict is expected:

1ai_response = InstallmentOption(months=10, total="R$ 960,60") # Pydantic object
2
3invoice_data = {
4 "installment_options": [ai_response] # ❌ Pydantic validation error!
5}
6
7invoice = InvoiceIn(**invoice_data) # FAILS with model_type error

Scenario 3: Converting Objects to Dicts (✅ Works)

This works - converting Pydantic objects to dictionaries first:

1ai_response = InstallmentOption(months=10, total="R$ 960,60")
2
3invoice_data = {
4 "installment_options": [ai_response.model_dump()] # ✅ Convert to dict
5}
6
7invoice = InvoiceIn(**invoice_data) # ✅ Success

Why This Happens

Pydantic Type Validation

When Pydantic validates a field like installment_options: List[CreditCardInstallmentOption], it expects:

  1. Dictionary → Converts to CreditCardInstallmentOption automatically
  2. Already a CreditCardInstallmentOption → Accepts directly
  3. Different Pydantic model (like InstallmentOption) → REJECTS even if fields match!

The Error Message Decoded

1Input should be a valid dictionary or instance of CreditCardInstallmentOption
2[type=model_type, input_value=InstallmentOption(months=10, total='R$ 960,60'),
3input_type=InstallmentOption]

Our Specific Case

What Was Happening

AI Response (from core/ai/models/responses.py):

1financial_data.installment_options = [
2 InstallmentOption(months=10, total="R$ 960,60"), # ❌ Wrong class
3 InstallmentOption(months=4, total="R$ 766,12"), # ❌ Wrong class
4]

Invoice Schema (from domains/invoices/schemas.py):

1class CreditCardRawInvoice(BaseModel):
2 installment_options: List[CreditCardInstallmentOption] # ✅ Expected class

The Fix

Convert Pydantic objects to dictionaries:

1"installment_options": [
2 opt.model_dump() for opt in financial_data.installment_options
3], # ✅ Now it's [{"months": 10, "total": "R$ 960,60"}]

Common Pydantic Patterns

1. Dictionary → Pydantic (✅ Always Works)

1data = {"name": "John", "age": 30}
2user = User(**data) # ✅ Pydantic creates object from dict

2. Pydantic → Dictionary

1user = User(name="John", age=30)
2data = user.model_dump() # ✅ {"name": "John", "age": 30}

3. Same-Class Assignment (✅ Works)

1user1 = User(name="John", age=30)
2user2 = User.model_validate(user1) # ✅ Same class, works

4. Different-Class Assignment (❌ Fails)

1person = Person(name="John", age=30) # Different class
2user = User.model_validate(person) # ❌ Fails even if same fields

Best Practices

1. Use .model_dump() for Cross-Domain Data

When passing data between different domains:

1ai_data = ai_response.model_dump()
2domain_object = DomainModel(**ai_data)

2. Keep Schema Consistency

Better: Use the same schema across domains:

1from app.domains.transactions.schemas import TransactionData
2
3# AI responses use TransactionData
4# Invoice schemas use TransactionData
5# No conversion needed!

3. Validate Early

Validate at boundaries:

1try:
2 invoice = InvoiceIn(**data)
3except ValidationError as e:
4 logger.error(f"Validation failed: {e}") # Handle gracefully

Why We Had This Issue

Our Solution Strategy

This pattern is super common in FastAPI applications where you have multiple layers (API, AI, Domain) with their own Pydantic models! 🚀