add with a whole-document pointer ("path": "") works when the document root is an object but crashes when it is an array.
>>> import jsonpatch
>>> jsonpatch.apply_patch(['foo', 'bar'], [{'op': 'add', 'path': '', 'value': {'replaced': True}}])
TypeError: '>' not supported between instances of 'NoneType' and 'int'
move and copy to the root fail the same way on an array root, because both delegate to AddOperation:
>>> jsonpatch.apply_patch(['foo', 'bar'], [{'op': 'move', 'from': '/0', 'path': ''}])
TypeError: '>' not supported between instances of 'NoneType' and 'int'
>>> jsonpatch.apply_patch(['foo', 'bar'], [{'op': 'copy', 'from': '/1', 'path': ''}])
TypeError: '>' not supported between instances of 'NoneType' and 'int'
Why it is a defect rather than an unsupported case
replace at the root already works on an array root — it returns the new document.
add at the root already works on an object root.
- A bare
TypeError is not part of the documented exception hierarchy, so a caller writing except jsonpatch.JsonPatchException: cannot catch it.
RFC 6902 has exactly one root location, so all four combinations should agree.
Root cause
AddOperation.apply obtains subobj, part = self.pointer.to_last(obj), and a whole-document pointer yields part is None. The MutableMapping branch handles that (obj = value # we're replacing the root); the MutableSequence branch went straight to elif part > len(subobj), comparing None against an int. ReplaceOperation.apply checks if part is None: return value before branching, so it never hit this.
PR #15 ("Fixed replacing whole document", merged 2013) added exactly this handling to the code that existed then; the class-based refactor in b8083d70 (2020) kept it in the mapping branch and the sequence branch never received it.
Suggested fix
Give the MutableSequence branch the same part is None arm as the mapping branch, returning value. No test covers the array root.
addwith a whole-document pointer ("path": "") works when the document root is an object but crashes when it is an array.moveandcopyto the root fail the same way on an array root, because both delegate toAddOperation:Why it is a defect rather than an unsupported case
replaceat the root already works on an array root — it returns the new document.addat the root already works on an object root.TypeErroris not part of the documented exception hierarchy, so a caller writingexcept jsonpatch.JsonPatchException:cannot catch it.RFC 6902 has exactly one root location, so all four combinations should agree.
Root cause
AddOperation.applyobtainssubobj, part = self.pointer.to_last(obj), and a whole-document pointer yieldspart is None. TheMutableMappingbranch handles that (obj = value # we're replacing the root); theMutableSequencebranch went straight toelif part > len(subobj), comparingNoneagainst anint.ReplaceOperation.applychecksif part is None: return valuebefore branching, so it never hit this.PR #15 ("Fixed replacing whole document", merged 2013) added exactly this handling to the code that existed then; the class-based refactor in
b8083d70(2020) kept it in the mapping branch and the sequence branch never received it.Suggested fix
Give the
MutableSequencebranch the samepart is Nonearm as the mapping branch, returningvalue. No test covers the array root.