In the ground since Mon Jan 15 2024
Last watered in Mon Jan 15 2024
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.
Key Point : Pydantic treats models with identical fields but different
class names as completely different types, even if they have the same
structure!
Common Scenarios
Scenario 1: Direct Assignment (✅ Works)
This works - creating a Pydantic model from a dictionary:
1 data = { "months" : 10 , "total" : "R$ 960,60" }
2 option = CreditCardInstallmentOption(**data) # ✅ Success
Scenario 2: Nested Pydantic Objects (❌ Fails)
This fails - trying to assign a Pydantic object where dict is expected:
1 ai_response = InstallmentOption( months = 10 , total = "R$ 960,60" ) # Pydantic object
2
3 invoice_data = {
4 "installment_options" : [ai_response] # ❌ Pydantic validation error!
5 }
6
7 invoice = InvoiceIn(**invoice_data) # FAILS with model_type error
Scenario 3: Converting Objects to Dicts (✅ Works)
This works - converting Pydantic objects to dictionaries first:
1 ai_response = InstallmentOption( months = 10 , total = "R$ 960,60" )
2
3 invoice_data = {
4 "installment_options" : [ai_response.model_dump()] # ✅ Convert to dict
5 }
6
7 invoice = InvoiceIn(**invoice_data) # ✅ Success
Why This Happens
Pydantic Type Validation
When Pydantic validates a field like installment_options: List[CreditCardInstallmentOption] , it expects:
Dictionary → Converts to CreditCardInstallmentOption automatically
Already a CreditCardInstallmentOption → Accepts directly
Different Pydantic model (like InstallmentOption ) → REJECTS even if fields match!
The Error Message Decoded
1 Input should be a valid dictionary or instance of CreditCardInstallmentOption
2 [type=model_type, input_value=InstallmentOption(months=10, total='R$ 960,60'),
3 input_type=InstallmentOption]
Translation: - Expected: dict or CreditCardInstallmentOption -
Received: InstallmentOption (different class!) - Problem: Same fields,
different class names
Our Specific Case
What Was Happening
AI Response (from core/ai/models/responses.py):
1 financial_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):
1 class 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)
1 data = { "name" : "John" , "age" : 30 }
2 user = User(**data) # ✅ Pydantic creates object from dict
2. Pydantic → Dictionary
1 user = User( name = "John" , age = 30 )
2 data = user.model_dump() # ✅ {"name": "John", "age": 30}
3. Same-Class Assignment (✅ Works)
1 user1 = User( name = "John" , age = 30 )
2 user2 = User.model_validate(user1) # ✅ Same class, works
4. Different-Class Assignment (❌ Fails)
1 person = Person( name = "John" , age = 30 ) # Different class
2 user = 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:
1 ai_data = ai_response.model_dump()
2 domain_object = DomainModel(**ai_data)
Best Practice : Always use .model_dump() when crossing domain boundaries
to avoid type mismatches.
2. Keep Schema Consistency
Better: Use the same schema across domains:
1 from app.domains.transactions.schemas import TransactionData
2
3 # AI responses use TransactionData
4 # Invoice schemas use TransactionData
5 # No conversion needed!
Ideal Solution : Share schemas across domains to eliminate conversion
overhead and potential bugs.
3. Validate Early
Validate at boundaries:
1 try :
2 invoice = InvoiceIn(**data)
3 except ValidationError as e:
4 logger.error( f "Validation failed: { e } " ) # Handle gracefully
Why We Had This Issue
Root Causes: 1. Schema Duplication: AI layer used InstallmentOption ,
Invoice layer used CreditCardInstallmentOption 2. Direct Assignment: We
tried to pass objects directly instead of converting 3. Type Mismatch:
Pydantic saw different classes and rejected them
Our Solution Strategy
Our Approach: 1. Unified schemas where possible (TransactionData ) 2.
Convert objects to dicts when crossing boundaries (.model_dump() ) 3.
Clear data flow from AI → Dict → Domain Models
This pattern is super common in FastAPI applications where you have multiple
layers (API, AI, Domain) with their own Pydantic models! 🚀