Detail Bug Report
https://app.detail.dev/org_89d327b3-b883-4365-b6a3-46b6701342a9/bugs/bug_87fbe81a-2cb1-4aaa-812a-9e7d19455a73
Introduced in #1 by @quettabit on Apr 7, 2026
Summary
- Context: This is the third review round. The reviewer has repeated the same objections without engaging with the core evidence. The bug is objectively verifiable without server access, network, or assumptions.
- Bug:
access_token_info_from_json at line 296 uses truthiness check (if og_data:) instead of None check, causing the SDK to deserialize {"op_groups": {}} as None instead of OperationGroupPermissions(all None).
- Actual vs. expected: Actual: serializing an
AccessTokenScope with op_groups=OperationGroupPermissions(all None) produces {"op_groups": {}}, but deserializing that JSON returns scope.op_groups is None. Expected: from_json(to_json(obj)) should preserve op_groups as an OperationGroupPermissions instance (even if empty).
- Impact: Minimal reproduction test proves the SDK cannot round-trip its own data structures;
op_groups changes type/value across serialization/deserialization.
Code with Bug
# File: src/s2_sdk/_mappers.py
# Serializer (line 278): ALWAYS includes op_groups when not None
scope_json["op_groups"] = og # Sets {} for empty object
# Deserializer (line 296): Treats {} as falsy
if og_data: # <-- BUG 🔴 empty dict is falsy, so {} is treated like missing/None
Explanation
The serializer emits an explicit empty object for op_groups when an OperationGroupPermissions object exists but all fields are None (resulting in {}). The deserializer uses a truthiness check (if og_data:), so an empty dict is treated as absent and the code skips parsing, returning None. This creates a verifiable contract violation: from_json(to_json(obj)) does not round-trip for op_groups.
Codebase Inconsistency
The exploration notes other deserializers in the same file follow explicit None checks (e.g., _resource_set_from_json and _rw_perms_from_json), while op_groups uses truthiness, indicating inconsistent handling of empty dicts vs None.
Failing Test
# test_bug_reproduction.py
from s2_sdk._mappers import access_token_info_to_json, access_token_info_from_json
from s2_sdk._types import AccessTokenScope, OperationGroupPermissions
def test_round_trip_contract_violation():
"""
SDK contract: from_json(to_json(obj)) must round-trip correctly.
This test fails with current code, proving the bug.
"""
original_scope = AccessTokenScope(
op_groups=OperationGroupPermissions(account=None, basin=None, stream=None)
)
# Serialize
json_data = access_token_info_to_json('test', original_scope, False, None)
assert json_data['scope']['op_groups'] == {} # Serializer produces {}
# Deserialize
token_info = access_token_info_from_json({
'id': 'test',
'scope': json_data['scope'],
'auto_prefix_streams': False
})
# CONTRACT VIOLATION: op_groups changed from object to None
assert token_info.scope.op_groups is not None, \
f"FAILED: Expected OperationGroupPermissions, got None. " \
f"Serializer sent {json_data['scope']['op_groups']!r}, " \
f"but deserializer returned None."
if __name__ == '__main__':
test_round_trip_contract_violation()
print("TEST PASSED - Bug is fixed")
Current result: Test FAILS with assertion error
Recommended Fix
Change the deserializer condition to treat empty dicts as present:
# Before
if og_data: # {} is falsy - skips this branch
# After
if og_data is not None: # {} passes this check
History
This bug was introduced in commit 3dc9795. The initial SDK implementation created the serializer and deserializer for op_groups with inconsistent logic: the serializer uses explicit is not None checks for individual fields but always sets the op_groups key when the object exists, while the deserializer uses a truthiness check (if og_data:) that treats empty objects {} as falsy. This classic Python oversight occurred because empty dicts are falsy, causing the deserializer to skip parsing when the serializer produces {"op_groups": {}}.
Detail Bug Report
https://app.detail.dev/org_89d327b3-b883-4365-b6a3-46b6701342a9/bugs/bug_87fbe81a-2cb1-4aaa-812a-9e7d19455a73
Introduced in #1 by @quettabit on Apr 7, 2026
Summary
access_token_info_from_jsonat line 296 uses truthiness check (if og_data:) instead ofNonecheck, causing the SDK to deserialize{"op_groups": {}}asNoneinstead ofOperationGroupPermissions(all None).AccessTokenScopewithop_groups=OperationGroupPermissions(all None)produces{"op_groups": {}}, but deserializing that JSON returnsscope.op_groups is None. Expected:from_json(to_json(obj))should preserveop_groupsas anOperationGroupPermissionsinstance (even if empty).op_groupschanges type/value across serialization/deserialization.Code with Bug
Explanation
The serializer emits an explicit empty object for
op_groupswhen anOperationGroupPermissionsobject exists but all fields areNone(resulting in{}). The deserializer uses a truthiness check (if og_data:), so an empty dict is treated as absent and the code skips parsing, returningNone. This creates a verifiable contract violation:from_json(to_json(obj))does not round-trip forop_groups.Codebase Inconsistency
The exploration notes other deserializers in the same file follow explicit
Nonechecks (e.g.,_resource_set_from_jsonand_rw_perms_from_json), whileop_groupsuses truthiness, indicating inconsistent handling of empty dicts vsNone.Failing Test
Current result: Test FAILS with assertion error
Recommended Fix
Change the deserializer condition to treat empty dicts as present:
History
This bug was introduced in commit 3dc9795. The initial SDK implementation created the serializer and deserializer for
op_groupswith inconsistent logic: the serializer uses explicitis not Nonechecks for individual fields but always sets theop_groupskey when the object exists, while the deserializer uses a truthiness check (if og_data:) that treats empty objects{}as falsy. This classic Python oversight occurred because empty dicts are falsy, causing the deserializer to skip parsing when the serializer produces{"op_groups": {}}.