From 13b9554a58bde556245a4ae1f6d74bc75be7c0ab Mon Sep 17 00:00:00 2001 From: Sakthivel Subramanian <179120858+sakthivelmanii@users.noreply.github.com> Date: Wed, 4 Nov 2026 17:09:14 +0000 Subject: [PATCH 1/3] fix(django-google-spanner): preserve JsonObject in JSONField adaptation and deserialization --- .../django_spanner/__init__.py | 12 ++++++++++++ .../django_spanner/operations.py | 10 ++++++++++ .../unit/django_spanner/test_operations.py | 19 +++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/packages/django-google-spanner/django_spanner/__init__.py b/packages/django-google-spanner/django_spanner/__init__.py index fc8cb56b2104..a442ed76c3e1 100644 --- a/packages/django-google-spanner/django_spanner/__init__.py +++ b/packages/django-google-spanner/django_spanner/__init__.py @@ -93,7 +93,19 @@ def get_prep_value(self, value): return value +old_jsonfield_from_db_value = getattr(JSONField, "from_db_value", None) + + +def jsonfield_from_db_value(self, value, expression, connection): + if isinstance(value, (dict, list, JsonObject)): + return value + if old_jsonfield_from_db_value: + return old_jsonfield_from_db_value(self, value, expression, connection) + return value + + JSONField.get_prep_value = get_prep_value +JSONField.from_db_value = jsonfield_from_db_value old_datetimewithnanoseconds_eq = getattr(DatetimeWithNanoseconds, "__eq__", None) diff --git a/packages/django-google-spanner/django_spanner/operations.py b/packages/django-google-spanner/django_spanner/operations.py index 5a5b8e3e4612..55b340b7dee4 100644 --- a/packages/django-google-spanner/django_spanner/operations.py +++ b/packages/django-google-spanner/django_spanner/operations.py @@ -20,6 +20,7 @@ TimestampStr, escape_name, ) +from google.cloud.spanner_v1 import JsonObject class DatabaseOperations(BaseDatabaseOperations): @@ -770,3 +771,12 @@ def savepoint_rollback_sql(self, sid): Return the SQL for rolling back to a savepoint. """ return "SELECT 1" + + def adapt_json_value(self, value, encoder): + if value is None: + return None + if isinstance(value, JsonObject): + return value + if isinstance(value, (dict, list)): + return JsonObject(value) + return super().adapt_json_value(value, encoder) diff --git a/packages/django-google-spanner/tests/unit/django_spanner/test_operations.py b/packages/django-google-spanner/tests/unit/django_spanner/test_operations.py index 520d268bce10..6b9b6c886526 100644 --- a/packages/django-google-spanner/tests/unit/django_spanner/test_operations.py +++ b/packages/django-google-spanner/tests/unit/django_spanner/test_operations.py @@ -347,3 +347,22 @@ def test_datetime_cast_sql(self): "TIMESTAMP(FORMAT_TIMESTAMP('%Y-%m-%d %R:%E9S %Z', field, 'UTC'))", ) self.assertIsNone(params) + + def test_adapt_json_value(self): + from django.db.models import JSONField + from google.cloud.spanner_v1 import JsonObject + + self.assertIsNone(self.db_operations.adapt_json_value(None, None)) + obj = JsonObject({"a": 1}) + self.assertIs(self.db_operations.adapt_json_value(obj, None), obj) + adapted_dict = self.db_operations.adapt_json_value({"a": 1}, None) + self.assertIsInstance(adapted_dict, JsonObject) + adapted_list = self.db_operations.adapt_json_value([1, 2], None) + self.assertIsInstance(adapted_list, JsonObject) + self.assertEqual(self.db_operations.adapt_json_value("str", None), '"str"') + + field = JSONField() + self.assertIs(field.from_db_value(obj, None, None), obj) + self.assertEqual(field.from_db_value({"a": 1}, None, None), {"a": 1}) + self.assertEqual(field.from_db_value('{"a": 1}', None, None), {"a": 1}) + self.assertIsNone(field.from_db_value(None, None, None)) From 18eaf5b74aa3cd5e777bd3e308b38ebdf7a0b5d0 Mon Sep 17 00:00:00 2001 From: Sakthivel Subramanian <179120858+sakthivelmanii@users.noreply.github.com> Date: Wed, 4 Nov 2026 17:43:08 +0000 Subject: [PATCH 2/3] Addressed comments --- .../django_spanner/__init__.py | 10 ++++++- .../django_spanner/operations.py | 8 +++--- .../unit/django_spanner/test_operations.py | 27 ++++++++++++++++--- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/packages/django-google-spanner/django_spanner/__init__.py b/packages/django-google-spanner/django_spanner/__init__.py index a442ed76c3e1..49d9f996d34c 100644 --- a/packages/django-google-spanner/django_spanner/__init__.py +++ b/packages/django-google-spanner/django_spanner/__init__.py @@ -97,7 +97,15 @@ def get_prep_value(self, value): def jsonfield_from_db_value(self, value, expression, connection): - if isinstance(value, (dict, list, JsonObject)): + if isinstance(value, JsonObject): + if value._is_null: + return None + if value._is_array: + return value._array_value + if value._is_scalar_value: + return value._simple_value + return dict(value) + if isinstance(value, (dict, list)): return value if old_jsonfield_from_db_value: return old_jsonfield_from_db_value(self, value, expression, connection) diff --git a/packages/django-google-spanner/django_spanner/operations.py b/packages/django-google-spanner/django_spanner/operations.py index 55b340b7dee4..4ed0f14c4e0e 100644 --- a/packages/django-google-spanner/django_spanner/operations.py +++ b/packages/django-google-spanner/django_spanner/operations.py @@ -776,7 +776,7 @@ def adapt_json_value(self, value, encoder): if value is None: return None if isinstance(value, JsonObject): - return value - if isinstance(value, (dict, list)): - return JsonObject(value) - return super().adapt_json_value(value, encoder) + if value._is_null or value._is_array or value._is_scalar_value: + return value + value = dict(value) + return JsonObject.from_str(super().adapt_json_value(value, encoder)) diff --git a/packages/django-google-spanner/tests/unit/django_spanner/test_operations.py b/packages/django-google-spanner/tests/unit/django_spanner/test_operations.py index 6b9b6c886526..2b5f1413a9be 100644 --- a/packages/django-google-spanner/tests/unit/django_spanner/test_operations.py +++ b/packages/django-google-spanner/tests/unit/django_spanner/test_operations.py @@ -349,20 +349,41 @@ def test_datetime_cast_sql(self): self.assertIsNone(params) def test_adapt_json_value(self): + import uuid + + from django.core.serializers.json import DjangoJSONEncoder from django.db.models import JSONField from google.cloud.spanner_v1 import JsonObject self.assertIsNone(self.db_operations.adapt_json_value(None, None)) obj = JsonObject({"a": 1}) - self.assertIs(self.db_operations.adapt_json_value(obj, None), obj) + self.assertEqual(self.db_operations.adapt_json_value(obj, None), {"a": 1}) adapted_dict = self.db_operations.adapt_json_value({"a": 1}, None) self.assertIsInstance(adapted_dict, JsonObject) + self.assertEqual(adapted_dict, {"a": 1}) adapted_list = self.db_operations.adapt_json_value([1, 2], None) self.assertIsInstance(adapted_list, JsonObject) - self.assertEqual(self.db_operations.adapt_json_value("str", None), '"str"') + self.assertEqual(adapted_list, [1, 2]) + adapted_str = self.db_operations.adapt_json_value("str", None) + self.assertIsInstance(adapted_str, JsonObject) + self.assertEqual(adapted_str, "str") + + u = uuid.UUID("12345678-1234-5678-1234-567812345678") + adapted_uuid = self.db_operations.adapt_json_value({"u": u}, DjangoJSONEncoder) + self.assertIsInstance(adapted_uuid, JsonObject) + self.assertEqual( + adapted_uuid, {"u": "12345678-1234-5678-1234-567812345678"} + ) field = JSONField() - self.assertIs(field.from_db_value(obj, None, None), obj) + self.assertEqual(field.from_db_value(obj, None, None), {"a": 1}) + self.assertEqual( + field.from_db_value(JsonObject([1, 2]), None, None), [1, 2] + ) + self.assertEqual( + field.from_db_value(JsonObject("str"), None, None), "str" + ) + self.assertIsNone(field.from_db_value(JsonObject(), None, None)) self.assertEqual(field.from_db_value({"a": 1}, None, None), {"a": 1}) self.assertEqual(field.from_db_value('{"a": 1}', None, None), {"a": 1}) self.assertIsNone(field.from_db_value(None, None, None)) From d30c74b60abb6e691d7640b455dccd933e30bc8d Mon Sep 17 00:00:00 2001 From: Sakthivel Subramanian <179120858+sakthivelmanii@users.noreply.github.com> Date: Wed, 4 Nov 2026 17:52:17 +0000 Subject: [PATCH 3/3] style(django-google-spanner): format test_operations.py with ruff --- .../tests/unit/django_spanner/test_operations.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/django-google-spanner/tests/unit/django_spanner/test_operations.py b/packages/django-google-spanner/tests/unit/django_spanner/test_operations.py index 2b5f1413a9be..7d9aa7bc6fa1 100644 --- a/packages/django-google-spanner/tests/unit/django_spanner/test_operations.py +++ b/packages/django-google-spanner/tests/unit/django_spanner/test_operations.py @@ -371,18 +371,12 @@ def test_adapt_json_value(self): u = uuid.UUID("12345678-1234-5678-1234-567812345678") adapted_uuid = self.db_operations.adapt_json_value({"u": u}, DjangoJSONEncoder) self.assertIsInstance(adapted_uuid, JsonObject) - self.assertEqual( - adapted_uuid, {"u": "12345678-1234-5678-1234-567812345678"} - ) + self.assertEqual(adapted_uuid, {"u": "12345678-1234-5678-1234-567812345678"}) field = JSONField() self.assertEqual(field.from_db_value(obj, None, None), {"a": 1}) - self.assertEqual( - field.from_db_value(JsonObject([1, 2]), None, None), [1, 2] - ) - self.assertEqual( - field.from_db_value(JsonObject("str"), None, None), "str" - ) + self.assertEqual(field.from_db_value(JsonObject([1, 2]), None, None), [1, 2]) + self.assertEqual(field.from_db_value(JsonObject("str"), None, None), "str") self.assertIsNone(field.from_db_value(JsonObject(), None, None)) self.assertEqual(field.from_db_value({"a": 1}, None, None), {"a": 1}) self.assertEqual(field.from_db_value('{"a": 1}', None, None), {"a": 1})