pythoniterablejsonpickle

Implementing iterable for jsonpickle decoded twice


When I encode and decode a custom iterable class with jsonpickle, the contained items are doubled.

I tried to use demjson and simplejson and I tried to implement this https://docs.python.org/2.5/ref/sequence-types.html. If I inherit from list it does work. But I don't want to inherit. It also works if I do not implement iter

I have a class like this:

import jsonpickle
from typing import *

class Product:
    def __init__(self, name):
        self.name = name

class Products:
    def __init__(self):
        self.__products: List[Product] = list()

    def append(self, product: Product):
        self.__products.append(product)

    def __iter__(self):
        return iter(self.__products)

    def __next__(self):
        return next(self.__products)

    def __len__(self):
        return len(self.__products)

    def __getitem__(self, i):
        return self.__products[i]

    def extend(self, products: Iterable[Product]):
        self.__products.extend(products)

When I use jsonpickle to encode this class and decode it again the contained products are doubled. The ValueError is raised in this example

if __name__ == '__main__':
    products = Products()
    products.append(Product('abc'))
    encoded = jsonpickle.encode(products)
    decoded_products = jsonpickle.decode(encoded)
    if len(decoded_products) == 2:
        raise ValueError()

If I use encoded = jsonpickle.encode(products, make_refs=False) the second object is a string and not product

Do I have to implement any other method so that it works correctly?


Solution

  • I think jsonpickle is confused by the object, that looks like a sequence.

    When decoding it, it first sets the value of __products as a complete list and later calls append for each element again.

    I am not entirely sure why this happens, but you can visualize it using the following code inside Products:

    def __setattr__(self, name, value):
        super().__setattr__(name, value)
        print("set", name, value)
    
    def append(self, product: Product):
        print("append", product)
        self.__products.append(product)
    

    You can fix it by implementing a custom pickle protocol like so:

    class Products:
        def __getstate__(self):
            return self.__products
    
        def __setstate__(self, state):
            self.__products = state