Repository: jsonpickle/jsonpickle (2022-05-11 ~ 2025-06-07)

problem_statement: Doesn't respect @property
Hey! When using ``jsonpickle.encode(Object)``, it doesn't serialize the methods marked with @property. For example:

```python
@property
def name(self):
    return 'John Smith'
```

Should yield the following json:
```json
{
  "name": "John Smith"
}
date: 2022-05-11
patch: diff --git a/CHANGES.rst b/CHANGES.rst
index 606a7cd..0bcc7b8 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -10,6 +10,8 @@ v3.0.0
     * Drop support for CPython<3.7. CPython 3.6 and below have reached EOL
       and no longer receieve security updates. (#375)
     * Remove jsonlib and yajl backends (py2 only)
+    * Add ``include_properties`` option to the pickler. This should only
+      be used if analyzing generated json outside of Python. (#297) (+387)
 
 v2.2.0
 ======
diff --git a/Makefile b/Makefile
index caa6eec..614da8c 100644
--- a/Makefile
+++ b/Makefile
@@ -44,7 +44,7 @@ TOXCMD += --develop --skip-missing-interpreters
 ifdef multi
     TOXCMD += --parallel $(NUM_JOBS)
     TOXCMD += -e
-    TOXCMD += 'clean,py{27,36,37,38,39},py{27,38,39}-sa{12,13},py{27,38,39}-libs'
+    TOXCMD += 'clean,py{37,38,39,310},py{38,39,310}-sa{12,13},py{38,39,310}-libs'
     # Disable coverage when running in parallel
     TOXCMD += -- --no-cov
 endif
@@ -55,7 +55,7 @@ ifdef V
 endif
 
 # Coverage
-COVERAGE_ENV ?= py39
+COVERAGE_ENV ?= py310
 
 # Data
 ARTIFACTS := build
@@ -106,5 +106,5 @@ clean::
 .PHONY: clean
 
 format::
-	$(BLACK) --skip-string-normalization --target-version py27 $(PYTHON_DIRS)
+	$(BLACK) --skip-string-normalization --target-version py310 $(PYTHON_DIRS)
 .PHONY: format
diff --git a/jsonpickle/compat.py b/jsonpickle/compat.py
index 43e3744..320c64d 100644
--- a/jsonpickle/compat.py
+++ b/jsonpickle/compat.py
@@ -1,19 +1,14 @@
 from __future__ import absolute_import, division, unicode_literals
 
-import base64
+import queue  # noqa
 import sys
-import types
+from collections.abc import Iterator as abc_iterator  # noqa
 
 PY_MAJOR = sys.version_info[0]
 
 class_types = (type,)
 iterator_types = (type(iter('')),)
 
-import builtins
-import queue
-from base64 import decodebytes, encodebytes
-from collections.abc import Iterator as abc_iterator
-
 string_types = (str,)
 numeric_types = (int, float)
 ustr = str
diff --git a/jsonpickle/pickler.py b/jsonpickle/pickler.py
index 1b761f2..f7062f3 100644
--- a/jsonpickle/pickler.py
+++ b/jsonpickle/pickler.py
@@ -10,7 +10,6 @@ import decimal
 import inspect
 import itertools
 import sys
-import types
 import warnings
 from itertools import chain, islice
 
@@ -307,13 +306,6 @@ class Pickler(object):
             self.reset()
         return self._flatten(obj)
 
-    def _flatten_file(self, obj):
-        """
-        Special case file objects
-        """
-        assert not PY3 and isinstance(obj, types.FileType)
-        return None
-
     def _flatten_bytestring(self, obj):
         return {self._bytes_tag: self._bytes_encoder(obj)}
 
@@ -381,66 +373,94 @@ class Pickler(object):
 
         return data
 
-    def _get_flattener(self, obj):
-        if type(obj) in (list, dict):
-            if self._mkref(obj):
-                return (
-                    self._list_recurse if type(obj) is list else self._flatten_dict_obj
-                )
-            else:
-                self._push()
-                return self._getref
+    def _getstate(self, obj, data):
+        state = self._flatten(obj)
+        if self.unpicklable:
+            data[tags.STATE] = state
+        else:
+            data = state
+        return data
 
-        # We handle tuples and sets by encoding them in a "(tuple|set)dict"
-        elif type(obj) in (tuple, set):
-            if not self.unpicklable:
-                return self._list_recurse
-            return lambda obj: {
-                tags.TUPLE
-                if type(obj) is tuple
-                else tags.SET: [self._flatten(v) for v in obj]
-            }
+    def _flatten_key_value_pair(self, k, v, data):
+        """Flatten a key/value pair into the passed-in dictionary."""
+        if not util.is_picklable(k, v):
+            return data
 
-        elif util.is_object(obj):
-            return self._ref_obj_instance
+        if k is None:
+            k = 'null'  # for compatibility with common json encoders
 
-        elif util.is_type(obj):
-            return _mktyperef
+        if self.numeric_keys and isinstance(k, numeric_types):
+            pass
+        elif not isinstance(k, string_types):
+            try:
+                k = repr(k)
+            except Exception:
+                k = compat.ustr(k)
 
-        elif util.is_module_function(obj):
-            return self._flatten_function
+        data[k] = self._flatten(v)
+        return data
 
-        # instance methods, lambdas, old style classes...
-        self._pickle_warning(obj)
-        return None
+    def _flatten_obj_attrs(self, obj, attrs, data):
+        flatten = self._flatten_key_value_pair
+        ok = False
+        for k in attrs:
+            try:
+                if not k.startswith('__'):
+                    value = getattr(obj, k)
+                else:
+                    value = getattr(obj, f"_{obj.__class__.__name__}{k}")
+                flatten(k, value, data)
+            except AttributeError:
+                # The attribute may have been deleted
+                continue
+            ok = True
+        return ok
 
-    def _ref_obj_instance(self, obj):
-        """Reference an existing object or flatten if new"""
-        if self.unpicklable:
-            if self._mkref(obj):
-                # We've never seen this object so return its
-                # json representation.
-                return self._flatten_obj_instance(obj)
-            # We've seen this object before so place an object
-            # reference tag in the data. This avoids infinite recursion
-            # when processing cyclical objects.
-            return self._getref(obj)
-        else:
-            max_reached = self._max_reached()
-            in_cycle = _in_cycle(obj, self._objs, max_reached, False)
-            if in_cycle:
-                # A circular becomes None.
-                return None
+    def _flatten_properties(self, obj, data, allslots=None):
+        if allslots is None:
+            # setting a list as a default argument can lead to some weird errors
+            allslots = []
 
-            self._mkref(obj)
-            return self._flatten_obj_instance(obj)
+        # convert to set in case there are a lot of slots
+        allslots_set = set(itertools.chain.from_iterable(allslots))
+
+        # i don't like lambdas
+        def valid_property(x):
+            return not x[0].startswith("__") and x[0] not in allslots_set
+
+        properties = [
+            x[0] for x in inspect.getmembers(obj.__class__) if valid_property(x)
+        ]
+
+        properties_dict = {}
+        for p_name in properties:
+            p_val = getattr(obj, p_name)
+            if util.is_not_class(p_val):
+                properties_dict[p_name] = p_val
+            else:
+                properties_dict[p_name] = self._flatten(p_val)
+
+        data[tags.PROPERTY] = properties_dict
+
+        return data
+
+    def _flatten_newstyle_with_slots(self, obj, data):
+        """Return a json-friendly dict for new-style objects with __slots__."""
+        allslots = [
+            _wrap_string_slot(getattr(cls, '__slots__', tuple()))
+            for cls in obj.__class__.mro()
+        ]
+
+        # add properties to the attribute list
+        if self.include_properties:
+            data = self._flatten_properties(obj, data, allslots)
+
+        if not self._flatten_obj_attrs(obj, chain(*allslots), data):
+            attrs = [
+                x for x in dir(obj) if not x.startswith('__') and not x.endswith('__')
+            ]
+            self._flatten_obj_attrs(obj, attrs, data)
 
-    def _getstate(self, obj, data):
-        state = self._flatten(obj)
-        if self.unpicklable:
-            data[tags.STATE] = state
-        else:
-            data = state
         return data
 
     def _flatten_obj_instance(self, obj):
@@ -474,6 +494,9 @@ class Pickler(object):
 
         reduce_val = None
 
+        if self.include_properties:
+            data = self._flatten_properties(obj, data)
+
         if self.unpicklable:
             if has_reduce and not has_reduce_ex:
                 try:
@@ -608,6 +631,27 @@ class Pickler(object):
         self._pickle_warning(obj)
         return None
 
+    def _ref_obj_instance(self, obj):
+        """Reference an existing object or flatten if new"""
+        if self.unpicklable:
+            if self._mkref(obj):
+                # We've never seen this object so return its
+                # json representation.
+                return self._flatten_obj_instance(obj)
+            # We've seen this object before so place an object
+            # reference tag in the data. This avoids infinite recursion
+            # when processing cyclical objects.
+            return self._getref(obj)
+        else:
+            max_reached = self._max_reached()
+            in_cycle = _in_cycle(obj, self._objs, max_reached, False)
+            if in_cycle:
+                # A circular becomes None.
+                return None
+
+            self._mkref(obj)
+            return self._flatten_obj_instance(obj)
+
     def _escape_key(self, k):
         return tags.JSON_KEY + encode(
             k,
@@ -702,86 +746,38 @@ class Pickler(object):
 
         return data
 
-    def _flatten_obj_attrs(self, obj, attrs, data):
-        flatten = self._flatten_key_value_pair
-        ok = False
-        for k in attrs:
-            try:
-                if not k.startswith('__'):
-                    value = getattr(obj, k)
-                else:
-                    value = getattr(obj, f"_{obj.__class__.__name__}{k}")
-                flatten(k, value, data)
-            except AttributeError:
-                # The attribute may have been deleted
-                continue
-            ok = True
-        return ok
-
-    def _flatten_properties(self, obj, data, allslots=None):
-        if allslots is None:
-            # setting a list as a default argument can lead to some weird errors
-            allslots = []
-
-        properties = []
-
-        # convert to set in case there are a lot of slots
-        allslots_set = set(itertools.chain.from_iterable(allslots))
-
-        for cls in obj.__class__.mro():
-
-            # i don't like lambdas
-            def valid_property(x):
-                return not x[0].startswith("__") and x[0] not in allslots_set
-
-            # this could be a list comprehension but split it for readability
-            for cls in obj.__class__.mro():
-                properties += [
-                    x[0] for x in inspect.getmembers(cls) if valid_property(x)
-                ]
-
-        # deduplicate strings
-        data[tags.PROPERTY] = list(dict.fromkeys(properties))
-
-        return data
-
-    def _flatten_newstyle_with_slots(self, obj, data):
-        """Return a json-friendly dict for new-style objects with __slots__."""
-        allslots = [
-            _wrap_string_slot(getattr(cls, '__slots__', tuple()))
-            for cls in obj.__class__.mro()
-        ]
-
-        # add properties to the attribute list
-        if self.include_properties:
-            data = self._flatten_properties(obj, data, allslots)
-
-        if not self._flatten_obj_attrs(obj, chain(*allslots), data):
-            attrs = [
-                x for x in dir(obj) if not x.startswith('__') and not x.endswith('__')
-            ]
-            self._flatten_obj_attrs(obj, attrs, data)
+    def _get_flattener(self, obj):
+        if type(obj) in (list, dict):
+            if self._mkref(obj):
+                return (
+                    self._list_recurse if type(obj) is list else self._flatten_dict_obj
+                )
+            else:
+                self._push()
+                return self._getref
 
-        return data
+        # We handle tuples and sets by encoding them in a "(tuple|set)dict"
+        elif type(obj) in (tuple, set):
+            if not self.unpicklable:
+                return self._list_recurse
+            return lambda obj: {
+                tags.TUPLE
+                if type(obj) is tuple
+                else tags.SET: [self._flatten(v) for v in obj]
+            }
 
-    def _flatten_key_value_pair(self, k, v, data):
-        """Flatten a key/value pair into the passed-in dictionary."""
-        if not util.is_picklable(k, v):
-            return data
+        elif util.is_object(obj):
+            return self._ref_obj_instance
 
-        if k is None:
-            k = 'null'  # for compatibility with common json encoders
+        elif util.is_type(obj):
+            return _mktyperef
 
-        if self.numeric_keys and isinstance(k, numeric_types):
-            pass
-        elif not isinstance(k, string_types):
-            try:
-                k = repr(k)
-            except Exception:
-                k = compat.ustr(k)
+        elif util.is_module_function(obj):
+            return self._flatten_function
 
-        data[k] = self._flatten(v)
-        return data
+        # instance methods, lambdas, old style classes...
+        self._pickle_warning(obj)
+        return None
 
     def _flatten_sequence_obj(self, obj, data):
         """Return a json-friendly dict for a sequence subclass."""
diff --git a/jsonpickle/unpickler.py b/jsonpickle/unpickler.py
index 5bebcda..1bb3e69 100644
--- a/jsonpickle/unpickler.py
+++ b/jsonpickle/unpickler.py
@@ -808,6 +808,9 @@ class Unpickler(object):
                 self._namestack.pop()
         return data
 
+    def _restore_tuple(self, obj):
+        return tuple([self._restore(v) for v in obj[tags.TUPLE]])
+
     def _restore_tags(self, obj):
         try:
             if not tags.RESERVED <= set(obj) and not type(obj) in (list, dict):
@@ -855,6 +858,3 @@ class Unpickler(object):
                 return x
 
         return restore
-
-    def _restore_tuple(self, obj):
-        return tuple([self._restore(v) for v in obj[tags.TUPLE]])
diff --git a/jsonpickle/util.py b/jsonpickle/util.py
index 6e6f108..92ae3cd 100644
--- a/jsonpickle/util.py
+++ b/jsonpickle/util.py
@@ -20,13 +20,7 @@ import time
 import types
 
 from . import compat, tags
-from .compat import (
-    abc_iterator,
-    class_types,
-    iterator_types,
-    numeric_types,
-)
-
+from .compat import abc_iterator, class_types, iterator_types, numeric_types
 
 SEQUENCES = (list, set, tuple)
 SEQUENCES_SET = {list, set, tuple}
@@ -40,8 +34,6 @@ FUNCTION_TYPES = {
 }
 NON_REDUCIBLE_TYPES = (
     {
-        int,
-        float,
         list,
         dict,
         set,
@@ -52,6 +44,13 @@ NON_REDUCIBLE_TYPES = (
     | PRIMITIVES
     | FUNCTION_TYPES
 )
+NON_CLASS_TYPES = {
+    list,
+    dict,
+    set,
+    tuple,
+    bytes,
+} | PRIMITIVES
 
 
 def is_type(obj):
@@ -135,6 +134,13 @@ def is_object(obj):
     )
 
 
+def is_not_class(obj):
+    """Determines if the object is not a class or a class instance.
+    Used for serializing properties.
+    """
+    return type(obj) in NON_CLASS_TYPES
+
+
 def is_primitive(obj):
     """Helper method to see if the object is a basic data type. Unicode strings,
     integers, longs, floats, booleans, and None are considered primitive

test_patch: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index c8c4753..9ee8223 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -11,7 +11,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        python-version: ["2.7", "3.6", "3.7", "3.8", "3.9", "3.10"]
+        python-version: ["3.7", "3.8", "3.9", "3.10"]
         os: [ubuntu-20.04]
         env:
           - TOXENV: libs
diff --git a/tests/jsonpickle_test.py b/tests/jsonpickle_test.py
index 7235755..a9b5f5d 100644
--- a/tests/jsonpickle_test.py
+++ b/tests/jsonpickle_test.py
@@ -31,7 +31,7 @@ class ListLike:
         return self.internal_list[i]
 
 
-class Thing(object):
+class Thing:
     def __init__(self, name):
         self.name = name
         self.child = None
@@ -39,14 +39,20 @@ class Thing(object):
     def __repr__(self):
         return 'Thing("%s")' % self.name
 
+    def __iter__(self):
+        for attr in [
+            x for x in getattr(self.__class__, "__dict__") if not x.startswith("__")
+        ]:
+            yield attr, getattr(self, attr)
+
 
-class Capture(object):
+class Capture:
     def __init__(self, *args, **kwargs):
         self.args = args
         self.kwargs = kwargs
 
 
-class ThingWithProps(object):
+class ThingWithProps:
     def _get_identity(self):
         keys = [self.dogs, self.monkies, self.name]
         return hash('-'.join([str(key) for key in keys]))
@@ -98,9 +104,9 @@ class UserDict(dict):
         self.valid = False
 
 
-class Outer(object):
-    class Middle(object):
-        class Inner(object):
+class Outer:
+    class Middle:
+        class Inner:
             pass
 
 
@@ -112,6 +118,52 @@ class MySlots:
         self.__beta = 1
 
 
+class MyPropertiesSlots:
+    __slots__ = ("alpha", "arr")
+
+    def __init__(self):
+        self.alpha = 1
+        self.arr = [1, 2, 3]
+
+    @property
+    def arr_len(self):
+        return len(self.arr)
+
+    @property
+    def other_object(self):
+        return Thing("Test")
+
+    def __eq__(self, other):
+        return (
+            self.alpha == other.alpha
+            and self.arr == other.arr
+            and self.arr_len == other.arr_len
+            and list(self.other_object) == list(other.other_object)
+        )
+
+
+class MyPropertiesDict:
+    def __init__(self):
+        self.alpha = 1
+        self.arr = [1, 2, 3]
+
+    @property
+    def arr_len(self):
+        return len(self.arr)
+
+    @property
+    def other_object(self):
+        return Thing("Test")
+
+    def __eq__(self, other):
+        return (
+            self.alpha == other.alpha
+            and self.arr == other.arr
+            and self.arr_len == other.arr_len
+            and list(self.other_object) == list(other.other_object)
+        )
+
+
 def on_missing_callback(class_name):
     # not actually a runtime problem but it doesn't matter
     warnings.warn("The unpickler couldn't find %s" % class_name, RuntimeWarning)
@@ -489,6 +541,18 @@ class PicklingTestCase(unittest.TestCase):
         beta = getattr(obj, "_" + obj.__class__.__name__ + "__beta", "(missing beta)")
         self.assertEqual(alpha, beta)
 
+    def test_include_properties_slots(self):
+        obj = MyPropertiesSlots()
+        dumped = jsonpickle.dumps(obj, include_properties=True)
+        self.assertTrue("py/property" in dumped)
+        self.assertEqual(obj, jsonpickle.loads(dumped))
+
+    def test_include_properties_dict(self):
+        obj = MyPropertiesDict()
+        dumped = jsonpickle.dumps(obj, include_properties=True)
+        self.assertTrue("py/property" in dumped)
+        self.assertEqual(obj, jsonpickle.loads(dumped))
+
 
 class JSONPickleTestCase(SkippableTest):
     def setUp(self):


----

problem_statement: Numpy handlers does not handle views correctly
When using the numpy handler, some classes cannot be serialized. One example is the LogisticRegression class in scikit-learn>=0.24:

[the following example uses scikit-learn==0.24 and the latest jsonpickle==3.0.1, although previous versions of jsonpickle have the same issue as well]

```python
import sklearn.datasets
from sklearn.linear_model import LogisticRegression
import jsonpickle

from jsonpickle.ext.numpy import register_handlers

register_handlers()


iris = sklearn.datasets.load_iris()
X = iris.data[:, :2]
y = (iris.target != 0) * 1

pipe = LogisticRegression(solver="lbfgs")

pipe.fit(X, y)

res = pipe.predict(X)

# Try serializing/deserializing
js = jsonpickle.dumps(pipe)

pipe2 = jsonpickle.loads(js)
```
This fails with the following error:
```
...
File ~/miniconda3/envs/dstools/lib/python3.8/site-packages/jsonpickle/ext/numpy.py:328, in NumpyNDArrayHandlerView.restore(self, data)
    324 else:
    325     # decode array view, which references the data of another array
    326     base = self.context.restore(base, reset=False)
    327     assert (
--> 328         base.flags.forc
    329     ), "Current implementation assumes base is C or F contiguous"
    331     arr = np.ndarray(
    332         buffer=base.data,
    333         dtype=self.restore_dtype(data).newbyteorder(data.get('byteorder', '|')),
   (...)
    336         strides=data.get('strides', None),
    337     )
    339     self.restore_flags(data, arr)

AttributeError: 'list' object has no attribute 'flags'
```

NOTE: if I do NOT use the numpy extension, i.e., if I comment the following lines:

```
# from jsonpickle.ext.numpy import register_handlers

# register_handlers()
```
then everything works. This seems to point to a specific issue with the numpy handler.
date: 2022-12-28
patch: diff --git a/jsonpickle/ext/numpy.py b/jsonpickle/ext/numpy.py
index 2214607..af2bf71 100644
--- a/jsonpickle/ext/numpy.py
+++ b/jsonpickle/ext/numpy.py
@@ -324,6 +324,9 @@ class NumpyNDArrayHandlerView(NumpyNDArrayHandlerBinary):
         else:
             # decode array view, which references the data of another array
             base = self.context.restore(base, reset=False)
+            if not isinstance(base, np.ndarray):
+                # the object is probably a nested list
+                base = np.array(base)
             assert (
                 base.flags.forc
             ), "Current implementation assumes base is C or F contiguous"

test_patch: diff --git a/tests/sklearn_test.py b/tests/sklearn_test.py
index 3eeab99..864cc29 100644
--- a/tests/sklearn_test.py
+++ b/tests/sklearn_test.py
@@ -4,6 +4,8 @@ import pytest
 
 try:
     import numpy as np
+    import sklearn.datasets
+    from sklearn.linear_model import LogisticRegression
     from sklearn.tree import DecisionTreeClassifier
 except ImportError:
     pytest.skip('sklearn is not available', allow_module_level=True)
@@ -53,5 +55,21 @@ def test_decision_tree():
         assert actual.get_depth() == classifier.get_depth()
 
 
+def test_nested_array_serialization():
+    iris = sklearn.datasets.load_iris()
+    X = iris.data[:, :2]
+    y = (iris.target != 0) * 1
+
+    pipe = LogisticRegression(solver="lbfgs")
+    pipe.fit(X, y)
+
+    # Try serializing/deserializing.
+    # Pipe is a LogisticRegression object that contains a nested array
+    js = jsonpickle.dumps(pipe)
+    pipe2 = jsonpickle.loads(js)
+    # coef_ only appears on an instance that has already been fit to something
+    assert np.all(pipe.coef_ == pipe2.coef_)
+
+
 if __name__ == '__main__':
     pytest.main([__file__])


----

problem_statement: Cannot decode subclass of exception with keyword parameter
I am using jsonpickle v4.1.1.

I am likely doing something dumb, but I am getting an error when trying to decode into a subclass of Exception with one keyword only argument. I can minimally reproduce this as follows:

```py
import jsonpickle

class Foo(Exception):
    def __init__(self, a, *, b):
        self.a = a
        self.b = b

>>> jsonpickle.encode(Foo("a", b="b"))
'{"py/reduce": [{"py/type": "__main__.Foo"}, {"py/tuple": ["a"]}, {"a": "a", "b": "b"}]}'

>>> jsonpickle.decode(jsonpickle.encode(Foo("a", b="b")))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/dfarr/Developer/resonatehq-examples/deep-research/.venv/lib/python3.12/site-packages/jsonpickle/unpickler.py", line 103, in decode
    return context.restore(data, reset=reset, classes=classes)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/dfarr/Developer/resonatehq-examples/deep-research/.venv/lib/python3.12/site-packages/jsonpickle/unpickler.py", line 420, in restore
    value = self._restore(obj)
            ^^^^^^^^^^^^^^^^^^
  File "/Users/dfarr/Developer/resonatehq-examples/deep-research/.venv/lib/python3.12/site-packages/jsonpickle/unpickler.py", line 402, in _restore
    return restore(obj)
           ^^^^^^^^^^^^
  File "/Users/dfarr/Developer/resonatehq-examples/deep-research/.venv/lib/python3.12/site-packages/jsonpickle/unpickler.py", line 555, in _restore_reduce
    stage1 = f(*args)
             ^^^^^^^^
TypeError: Foo.__init__() missing 1 required keyword-only argument: 'b'
```

I also get a similar error when Foo is defined with two positional arguments and I instantiate as `Foo("a", b="b")`. However, I do not get this error if I instantiate with two positional args in this case `Foo("a", "b")`
date: 2025-06-07
patch: diff --git a/jsonpickle/unpickler.py b/jsonpickle/unpickler.py
index 1bb3e69..b6cebcb 100644
--- a/jsonpickle/unpickler.py
+++ b/jsonpickle/unpickler.py
@@ -463,7 +463,12 @@ class Unpickler(object):
                 cls = self._restore(cls)
             stage1 = cls.__new__(cls, *args[1:])
         else:
-            stage1 = f(*args)
+            try:
+                stage1 = f(*args)
+            except TypeError:
+                # this happens when there are missing kwargs and args don't match so we bypass
+                # __init__ since the state dict will set all attributes immediately afterwards
+                stage1 = f.__new__(f, *args)
 
         if state:
             try:

test_patch: diff --git a/tests/jsonpickle_test.py b/tests/jsonpickle_test.py
index a9b5f5d..53454bd 100644
--- a/tests/jsonpickle_test.py
+++ b/tests/jsonpickle_test.py
@@ -164,6 +164,13 @@ class MyPropertiesDict:
         )
 
 
+class KwargsSubclassTest(Exception):
+    def __init__(self, a, *, b):
+        self.a = a
+        self.b = b
+        self.c = a + b
+
+
 def on_missing_callback(class_name):
     # not actually a runtime problem but it doesn't matter
     warnings.warn("The unpickler couldn't find %s" % class_name, RuntimeWarning)
@@ -1657,3 +1664,11 @@ def test_repeat_objects_are_expanded():
 
 if __name__ == '__main__':
     pytest.main([__file__])
+
+
+def test_exception_kwargs():
+    actual = KwargsSubclassTest('a', b='b')
+    encoded = jsonpickle.encode(actual)
+    decoded = jsonpickle.decode(encoded)
+    assert isinstance(decoded, type(actual))
+    assert actual.c == decoded.c