Skip to content

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

@waiho-gumloop

Description

@waiho-gumloop

Description

JsonObject subclasses dict but when wrapping arrays or scalars, it returns early from __init__ without calling super().__init__(). This leaves the dict parent empty, causing standard Python operations to silently return wrong results.

Reproduction

from google.cloud.spanner_v1 import JsonObject
import json

val = JsonObject([{"id": "m1", "content": "hello"}])

print(isinstance(val, dict))   # True — it IS a dict subclass
print(len(val))                 # 0 — expected 1
print(bool(val))                # False — expected True
print(list(val))                # [] — expected the list contents
print(json.dumps(val))          # "{}" — expected the JSON array
print(val.serialize())          # '[{"content":"hello","id":"m1"}]' — correct

Root cause

In data_types.py, the array and scalar paths return early without initializing the dict:

def __init__(self, *args, **kwargs):
    self._is_array = len(args) and isinstance(args[0], (list, tuple))

    if self._is_array:
        self._array_value = args[0]
        return  # ← dict parent never initialized

    if self._is_scalar_value:
        self._simple_value = args[0]
        return  # ← same issue

The dict path (JSON objects) works correctly because it falls through to super().__init__().

Impact

Any code that receives a JsonObject wrapping an array and uses standard Python operations (len(), bool(), for x in val, val or default, json.dumps()) gets silently wrong results. This is particularly dangerous because isinstance(val, dict) returns True, so type checks pass but the object doesn't behave like one.

The .serialize() method works correctly for all variants because it reads from the internal _array_value/_simple_value attributes directly.

Suggested fix

Override __len__, __bool__, __iter__, and __getitem__ to delegate to the internal storage for array/scalar variants. Or stop subclassing dict since the array/scalar variants don't use it.

Version

Tested on 3.55.0, confirmed identical source in 3.61.0 (latest).

Metadata

Metadata

Assignees

Labels

api: spannerIssues related to the googleapis/python-spanner API.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions