I have a Pydantic model representing a bank account holding transactions, which themselves are models that are nested on the account model. The transactions are held in a list on a hidden field and accessed through a computed field that applies some checks in the setter method.
Simplified Models:
class Account(BaseModel):
id: UUID = Field(default_factory=uuid4)
owner: Customer
_transactions: List[Transaction] = list()
@fields.computed_field()
@property
def transactions(self) -> List[Transaction]:
return self._transactions
@transactions.setter
def transactions(self, list_: List[Transaction]) -> None:
# DO SOME CHECKS
self._transactions = sorted_list
class Transaction(BaseModel):
id: UUID = Field(default_factory=uuid4)
date: datetime.date = Field(frozen=False)
amount: PositiveFloat = Field(frozen=False)
class Customer(BaseModel):
id: UUID = Field(default_factory=uuid4)
name: str = Field(frozen=False)
Serialization
I can successfully serialize an account to JSON, including the transactions:
x = my_account.model_dump_json()
x
Formatted output:
'{
"id":"1eeed00b-3bd7-48fa-ab6a-6979618ef723",
"owner":{
"id":"0cad242dad03-492b-9c6b-86f0b75f9c00",
"name":"Bob"
},
"transactions":[
{"id":"42738f1d-e998-4add-94b8-713afe25b525","date":"2012-01-01","amount":1.0}
]
}'
De-serialization (unexpected behavior)
However, when I try to reconstruct the model from the JSON string, the transactions field is left empty. No issue with the owner field, which holds the nested Customer model:
Account.model_validate_json(x)
Formatted output:
Account(
id=UUID('1eeed00b-3bd7-48fa-ab6a-6979618ef723'),
owner=Customer(
id=UUID('0cad242d-ad03-492b-9c6b-86f0b75f9c00'),
name='Bob'
),
transactions=[]
)
Is there a way to make the serialization process for a computed field reversible?
Credit to Yurii Motov for getting me most of the way to the answer. I was able to make this work using a model validator in 'wrap' mode, which allows access to the pre-validated dictionary (from the JSON) and access to the Account model instance.
Full code example:
class Account(BaseModel):
id: UUID = Field(default_factory=uuid4)
owner: Customer
_transactions: List[Transaction] = list()
@fields.computed_field()
@property
def transactions(self) -> List[Transaction]:
return self._transactions
@transactions.setter
def transactions(self, list_: List[Transaction]) -> None:
# DO SOME CHECKS
self._transactions = sorted_list
@model_validator(mode="wrap")
def validate_model(self, handler: ModelWrapValidatorHandler['Account']) -> 'Account':
_transactions = []
# self is the dictionary form of the JSON when model_validate_json
# is called
if isinstance(self, dict):
for t in self.get("transactions", []):
_transactions.append(Transaction.model_validate(t))
# The handler is called on self to do the model construction
validated_self = handler(self)
validated_self.transactions = _transactions
return validated_self
class Transaction(BaseModel):
id: UUID = Field(default_factory=uuid4)
date: datetime.date = Field(frozen=False)
amount: PositiveFloat = Field(frozen=False)
class Customer(BaseModel):
id: UUID = Field(default_factory=uuid4)
name: str = Field(frozen=False)
Account.model_validate_json(
'{
"id":"1eeed00b-3bd7-48fa-ab6a-6979618ef723",
"owner":{
"id":"0cad242dad03-492b-9c6b-86f0b75f9c00",
"name":"Bob"
},
"transactions":[
{
"id":"42738f1d-e998-4add-94b8-713afe25b525",
"date":"2012-01-01",
"amount":1.0
}
]
}'
)
```
Formatted output:
```python
Account(
id=UUID('1eeed00b-3bd7-48fa-ab6a-6979618ef723'),
owner=Customer(
id=UUID('0cad242d-ad03-492b-9c6b-86f0b75f9c00'),
name='Bob'
),
transactions=[
Transaction(
id=UUID('42738f1d-e998-4add-94b8-713afe25b525'),
date=datetime.date(2012, 2, 20),
amount=1.0
)
]
)
```