Repository: jsonpickle/jsonpickle (python)

Issue:
Dictionary identity not preserved through roundtrip
On jsonpickle 1.1:

```
>>> import jsonpickle
>>> x = {}; y = jsonpickle.decode(jsonpickle.encode([x, x]))
>>> y[0]['hi'] = 3
>>> y[1]
{}
```

I didn't see any warnings about this in the doc and `list`s seem to survive fine.  Adding a custom handler to try to trigger the id serialization didn't work.

Actually, trying with some more mutable builtin types - `set` identity doesn't appear to be preserved either.

This is an important use case for us since we need to serialize user data and can't reasonably request them to prevent duplicate dictionaries in their graphs.

Pull request description:
serialization: preserve dictionary identity when un/pickling
Register dictionaries as eligible for referencing.
Ensure that dictionary identity is preserved across a roundtrip.

Resolves #255

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index f97ac2a..0fe19e6 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -5,6 +5,9 @@ v1.5.1
     * ``decode()`` documentation improvements.  (+341)
     * Serialization of Pandas DataFrame objects that contain
       timedelta64[ns] dtypes are now supported.  (+330) (#331)
+    * Dictionary identity is now preserved.  For example, if the same
+      dictionary appears twice in a list, the reconstituted list
+      will now contain two references to the same dictionary.  (#255) (+332)
 
 v1.5.0
 ======
diff --git a/jsonpickle/pickler.py b/jsonpickle/pickler.py
index 12007ee..13226f6 100644
--- a/jsonpickle/pickler.py
+++ b/jsonpickle/pickler.py
@@ -342,7 +342,11 @@ class Pickler(object):
             return lambda obj: {tags.SET: [self._flatten(v) for v in obj]}
 
         if util.is_dictionary(obj):
-            return self._flatten_dict_obj
+            if self._mkref(obj):
+                return self._flatten_dict_obj
+            else:
+                self._push()
+                return self._getref
 
         if util.is_type(obj):
             return _mktyperef
diff --git a/jsonpickle/unpickler.py b/jsonpickle/unpickler.py
index 3c658b3..b3e1326 100644
--- a/jsonpickle/unpickler.py
+++ b/jsonpickle/unpickler.py
@@ -541,6 +541,7 @@ class Unpickler(object):
 
     def _restore_dict(self, obj):
         data = {}
+        self._mkref(data)
 
         # If we are decoding dicts that can have non-string keys then we
         # need to do a two-phase decode where the non-string keys are


Test patch:
diff --git a/tests/jsonpickle_test.py b/tests/jsonpickle_test.py
index d436159..606ceaa 100644
--- a/tests/jsonpickle_test.py
+++ b/tests/jsonpickle_test.py
@@ -1,22 +1,21 @@
 # Copyright (C) 2008 John Paulett (john -at- paulett.org)
-# Copyright (C) 2009-2018 David Aguilar (davvid -at- gmail.com)
+# Copyright (C) 2009-2021 David Aguilar (davvid -at- gmail.com)
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
 # you should have received as part of this distribution.
 from __future__ import absolute_import, division, unicode_literals
-import doctest
 import os
 import unittest
 import collections
 
+import pytest
+
 import jsonpickle
 import jsonpickle.backend
 import jsonpickle.handlers
-
 from jsonpickle import compat, tags, util
 from jsonpickle.compat import PY2, PY3
-
 from helper import SkippableTest
 
 
@@ -1530,6 +1529,15 @@ class PicklingProtocol2TestCase(SkippableTest):
         self.test_cyclical_objects_unpickleable_false(use_tuple=False)
 
 
+def test_dict_references_are_preserved():
+    data = {}
+    actual = jsonpickle.decode(jsonpickle.encode([data, data]))
+    assert isinstance(actual, list)
+    assert isinstance(actual[0], dict)
+    assert isinstance(actual[1], dict)
+    assert actual[0] is actual[1]
+
+
 def test_repeat_objects_are_expanded():
     """Ensure that all objects are present in the json output"""
     # When references are disabled we should create expanded copies
@@ -1555,17 +1563,5 @@ def test_repeat_objects_are_expanded():
     assert flattened['passengers'][0]['child']['name'] == 'bob'
 
 
-def suite():
-    suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(JSONPickleTestCase))
-    suite.addTest(unittest.makeSuite(PicklingTestCase))
-    suite.addTest(unittest.makeSuite(PicklingProtocol2TestCase))
-    suite.addTest(unittest.makeSuite(PicklingProtocol4TestCase))
-    suite.addTest(doctest.DocTestSuite(jsonpickle))
-    suite.addTest(doctest.DocTestSuite(jsonpickle.pickler))
-    suite.addTest(doctest.DocTestSuite(jsonpickle.unpickler))
-    return suite
-
-
 if __name__ == '__main__':
-    unittest.main(defaultTest='suite')
+    pytest.main([__file__])