From c437b6ca867cce83c82ccd010fb5f79497165969 Mon Sep 17 00:00:00 2001 From: eeshsaxena Date: Sat, 15 Aug 2026 12:53:18 +0530 Subject: [PATCH] Raise JsonPatchConflict instead of TypeError on invalid move/copy/remove targets move and copy read subobj[part] from the 'from' pointer, and remove does del subobj[part]. When the pointer ends in '-' (the array-append token) the part is a string used to index a list, and when it points into a string value the container is immutable; both raised a bare TypeError. Catch TypeError alongside KeyError/IndexError so these surface as JsonPatchConflict. --- jsonpatch.py | 13 ++++++++++--- tests.py | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/jsonpatch.py b/jsonpatch.py index d3fc26d..4b84898 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -246,7 +246,10 @@ def apply(self, obj): try: del subobj[part] - except (KeyError, IndexError) as ex: + except (KeyError, IndexError, TypeError) as ex: + # TypeError happens when subobj is not a mutable container, e.g. a + # pointer into a string value ("str object doesn't support item + # deletion"). msg = "can't remove a non-existent object '{0}'".format(part) raise JsonPatchConflict(msg) @@ -379,7 +382,9 @@ def apply(self, obj): subobj, part = from_ptr.to_last(obj) try: value = subobj[part] - except (KeyError, IndexError) as ex: + except (KeyError, IndexError, TypeError) as ex: + # TypeError happens when the 'from' pointer ends in '-' (the + # array-append token), so part is a string used to index a list. raise JsonPatchConflict(str(ex)) # If source and target are equal, this is a no-op @@ -489,7 +494,9 @@ def apply(self, obj): subobj, part = from_ptr.to_last(obj) try: value = copy.deepcopy(subobj[part]) - except (KeyError, IndexError) as ex: + except (KeyError, IndexError, TypeError) as ex: + # TypeError happens when the 'from' pointer ends in '-' (the + # array-append token), so part is a string used to index a list. raise JsonPatchConflict(str(ex)) obj = AddOperation({ diff --git a/tests.py b/tests.py index d9eea92..d11a778 100755 --- a/tests.py +++ b/tests.py @@ -764,6 +764,25 @@ def test_remove_keyerror_dict(self): patch_obj = [ { "op": "remove", "path": "/foo/non-existent"} ] self.assertRaises(jsonpatch.JsonPatchConflict, jsonpatch.apply_patch, src, patch_obj) + def test_copy_from_dash_on_array(self): + # 'from' ending in '-' indexes a list with a string; used to raise a + # bare TypeError. + src = [1, 2, 3] + patch_obj = [ { "op": "copy", "path": "/0", "from": "/-"} ] + self.assertRaises(jsonpatch.JsonPatchConflict, jsonpatch.apply_patch, src, patch_obj) + + def test_move_from_dash_on_array(self): + src = [1, 2, 3] + patch_obj = [ { "op": "move", "path": "/0", "from": "/-"} ] + self.assertRaises(jsonpatch.JsonPatchConflict, jsonpatch.apply_patch, src, patch_obj) + + def test_remove_index_into_string(self): + # A pointer into a string value is not a mutable container; deleting + # from it used to raise a bare TypeError. + src = {"foo": "bar"} + patch_obj = [ { "op": "remove", "path": "/foo/0"} ] + self.assertRaises(jsonpatch.JsonPatchConflict, jsonpatch.apply_patch, src, patch_obj) + def test_insert_oob(self): src = {"foo": [1, 2]} patch_obj = [ { "op": "add", "path": "/foo/10", "value": 1} ]