pythondeserializationpydantic

How to deserialize nested Pydantic models from JSON where nested models are held in a list on a computed field


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?


Solution

  • 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
            )
        ]
    )
    ```