From 6b4c0cd28a6c16ce6696edd1ba55ff6018ac4a10 Mon Sep 17 00:00:00 2001 From: chuenchen309 <48723787+chuenchen309@users.noreply.github.com> Date: Sun, 19 Jul 2026 14:37:32 +0800 Subject: [PATCH] Fix Delta JSON round-trip crash when orjson is not installed Delta serializes each iterable Opcode NamedTuple, then rebuilds it in the JSON deserializer with Opcode(**op_code), which assumes a mapping. That only holds under orjson: its default callback runs JSON_CONVERTOR and emits Opcodes as dicts. The stdlib json fallback (used on any install without the optional orjson extra) encodes NamedTuples natively as positional arrays, so Opcode(**op_code) raises TypeError and the documented round-trip Delta(dump, deserializer=json_loads) crashes for any list diff that goes through difflib opcodes (reorders / duplicates). Accept both shapes on deserialization: mapping -> Opcode(**op_code), array -> Opcode(*op_code) (fields are in NamedTuple order). Added a regression test that forces the builtin-json path via force_use_builtin_json=True so it guards the array shape even where orjson is installed. --- deepdiff/delta.py | 13 ++++++++----- tests/test_delta.py | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/deepdiff/delta.py b/deepdiff/delta.py index 0d1e33dc..224f3b3e 100644 --- a/deepdiff/delta.py +++ b/deepdiff/delta.py @@ -99,11 +99,14 @@ def _deserializer(obj, safe_to_import=None): for path, op_codes in result['_iterable_opcodes'].items(): _iterable_opcodes[path] = [] for op_code in op_codes: - _iterable_opcodes[path].append( - Opcode( - **op_code - ) - ) + # Serializers differ in how they encode the Opcode + # NamedTuple: orjson uses JSON_CONVERTOR and emits a + # mapping, but stdlib json (the fallback when orjson + # is not installed) encodes it as a positional array. + if isinstance(op_code, Mapping): + _iterable_opcodes[path].append(Opcode(**op_code)) + else: + _iterable_opcodes[path].append(Opcode(*op_code)) result['_iterable_opcodes'] = _iterable_opcodes return result diff --git a/tests/test_delta.py b/tests/test_delta.py index 38cac165..a973706e 100644 --- a/tests/test_delta.py +++ b/tests/test_delta.py @@ -2820,6 +2820,26 @@ def test_list_of_alphabet_and_its_delta(self): assert l2 == l1 + delta5 assert l1 == l2 - delta5 + def test_delta_iterable_opcodes_json_roundtrip_builtin_json(self): + # A list diff that goes through difflib opcodes (reorder / duplicates) + # serializes each Opcode NamedTuple. orjson emits it as a mapping, but + # the stdlib json fallback (used when orjson is not installed) emits it + # as a positional array; the deserializer must accept both. Forcing the + # builtin json path exercises the array shape even where orjson is + # installed, so a plain-install regression cannot hide behind orjson. + t1 = ['a', 'b', 'c', 'd', 'b', 'e'] + t2 = ['b', 'c', 'x', 'b', 'e', 'f'] + diff = DeepDiff(t1, t2) + + def builtin_json_dumps(item): + return json_dumps(item, force_use_builtin_json=True) + + dump = Delta(diff, bidirectional=True, serializer=builtin_json_dumps).dumps() + delta = Delta(dump, bidirectional=True, deserializer=json_loads) + + assert t1 + delta == t2 + assert t2 - delta == t1 + def test_delta_flat_rows(self): t1 = {"key1": "value1"} t2 = {"field2": {"key2": "value2"}}