Skip to content

fix(spanner): implement dict protocol and nested unwrapping for JsonObject - #17915

Open
sakthivelmanii wants to merge 1 commit into
mainfrom
feat-spanner-jsonobject-helpers
Open

fix(spanner): implement dict protocol and nested unwrapping for JsonObject#17915
sakthivelmanii wants to merge 1 commit into
mainfrom
feat-spanner-jsonobject-helpers

Conversation

@sakthivelmanii

Copy link
Copy Markdown
Contributor

Fixes JsonObject array/scalar/null variants breaking standard Python container protocols (len, bool, iter, getitem, contains, eq). Adds _unwrap_for_json helper to safely serialize nested JsonObject instances without data erasure.

Fixes #15870

Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly:

  • Make sure to open an issue as a bug/issue before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea
  • Ensure the tests and linter pass
  • Code coverage does not decrease (if any source code was changed)
  • Appropriate docs were updated (if necessary)

Fixes #<issue_number_goes_here> 🦕

…bject

Fixes JsonObject array/scalar/null variants breaking standard Python container protocols (len, bool, iter, getitem, contains, eq).
Adds _unwrap_for_json helper to safely serialize nested JsonObject instances without data erasure.

Fixes #15870
@sakthivelmanii
sakthivelmanii requested a review from a team as a code owner July 27, 2026 21:17

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements standard Python container protocols (such as length, truthiness, iteration, indexing, and equality) for JsonObject to support array, scalar, and null variants, and introduces a recursive unwrapping helper for serialization. The feedback recommends correcting the truthiness of scalar values to reflect their actual boolean value, simplifying equality checks by removing defensive getattr calls, preventing null objects from equating to empty dictionaries, and expanding unit tests to cover falsy scalars.

Comment on lines +74 to +75
if self._is_scalar_value:
return True

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation of __bool__ always returns True for any scalar value. However, JSON scalars can be falsy (e.g., False, 0, 0.0, ""). Returning True unconditionally for these values is incorrect. It should return the truthiness of the underlying scalar value.

Suggested change
if self._is_scalar_value:
return True
if self._is_scalar_value:
return bool(self._simple_value)

Comment on lines +107 to +124
if isinstance(other, JsonObject):
if self._is_array:
return (
getattr(other, "_is_array", False)
and self._array_value == other._array_value
)
if self._is_scalar_value:
return (
getattr(other, "_is_scalar_value", False)
and self._simple_value == other._simple_value
)
if self._is_null:
return getattr(other, "_is_null", False)
return not (
getattr(other, "_is_array", False)
or getattr(other, "_is_scalar_value", False)
or getattr(other, "_is_null", False)
) and super().__eq__(other)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since other is verified to be an instance of JsonObject via isinstance(other, JsonObject), and all JsonObject instances are guaranteed to have _is_array, _is_scalar_value, and _is_null initialized in __init__, we should access these attributes directly instead of using defensive getattr calls. This simplifies the code and improves readability.

Suggested change
if isinstance(other, JsonObject):
if self._is_array:
return (
getattr(other, "_is_array", False)
and self._array_value == other._array_value
)
if self._is_scalar_value:
return (
getattr(other, "_is_scalar_value", False)
and self._simple_value == other._simple_value
)
if self._is_null:
return getattr(other, "_is_null", False)
return not (
getattr(other, "_is_array", False)
or getattr(other, "_is_scalar_value", False)
or getattr(other, "_is_null", False)
) and super().__eq__(other)
if isinstance(other, JsonObject):
if self._is_array:
return (
other._is_array
and self._array_value == other._array_value
)
if self._is_scalar_value:
return (
other._is_scalar_value
and self._simple_value == other._simple_value
)
if self._is_null:
return other._is_null
return not (
other._is_array
or other._is_scalar_value
or other._is_null
) and super().__eq__(other)
References
  1. Do not use defensive getattr(self, 'attribute', None) checks for attributes that are guaranteed to be initialized in the class's __init__ method, as it adds unnecessary complexity.

Comment on lines +129 to +130
if self._is_null:
return other is None or (isinstance(other, dict) and len(other) == 0)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There is a semantic mismatch here: a JSON null (represented by JsonObject(None)) should not be considered equal to an empty JSON object {}. If we allow JsonObject(None) == {} to be True, it creates inconsistency because they serialize to completely different JSON values (null vs {}). We should restrict equality for null JsonObjects to only None or other null JsonObjects.

Suggested change
if self._is_null:
return other is None or (isinstance(other, dict) and len(other) == 0)
if self._is_null:
return other is None

Comment on lines +134 to +136
def test_scalar_bool(self):
obj = JsonObject(42)
self.assertTrue(obj)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current test only verifies truthiness for a truthy scalar (42). We should expand test_scalar_bool to also verify that falsy scalars (such as False, 0, and "") are correctly evaluated as falsy.

Suggested change
def test_scalar_bool(self):
obj = JsonObject(42)
self.assertTrue(obj)
def test_scalar_bool(self):
self.assertTrue(self._make_one(42))
self.assertFalse(self._make_one(False))
self.assertFalse(self._make_one(0))
self.assertFalse(self._make_one(""))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JsonObject array/scalar variants break standard dict protocol (len, bool, iteration)

1 participant