fix(spanner): implement dict protocol and nested unwrapping for JsonObject - #17915
fix(spanner): implement dict protocol and nested unwrapping for JsonObject#17915sakthivelmanii wants to merge 1 commit into
Conversation
…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
There was a problem hiding this comment.
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.
| if self._is_scalar_value: | ||
| return True |
There was a problem hiding this comment.
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.
| if self._is_scalar_value: | |
| return True | |
| if self._is_scalar_value: | |
| return bool(self._simple_value) |
| 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) |
There was a problem hiding this comment.
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.
| 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
- 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.
| if self._is_null: | ||
| return other is None or (isinstance(other, dict) and len(other) == 0) |
There was a problem hiding this comment.
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.
| if self._is_null: | |
| return other is None or (isinstance(other, dict) and len(other) == 0) | |
| if self._is_null: | |
| return other is None |
| def test_scalar_bool(self): | ||
| obj = JsonObject(42) | ||
| self.assertTrue(obj) |
There was a problem hiding this comment.
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.
| 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("")) |
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:
Fixes #<issue_number_goes_here> 🦕