Skip to content

Commit 22b42d9

Browse files
fix: metadata serialization for custom objects and large integers
- Fix JSON serialization error for custom objects in metadata by adding _make_json_serializable helper method that converts non-serializable objects to strings recursively - Fix precision loss for large integers (>2^53-1) by storing them as strings and converting back to int when deserializing - Update type hints to use union syntax instead of tuple for isinstance - All tests now pass including test_metadata_with_custom_objects and test_metadata_edge_cases
1 parent 5912b28 commit 22b42d9

File tree

2 files changed

+170
-5
lines changed

2 files changed

+170
-5
lines changed

src/a2a/utils/proto_utils.py

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,53 @@ def metadata(
4747
if metadata is None:
4848
return None
4949
return struct_pb2.Struct(
50-
# TODO: Add support for other types.
5150
fields={
52-
key: struct_pb2.Value(string_value=value)
51+
key: cls._convert_value_to_proto(value)
5352
for key, value in metadata.items()
54-
if isinstance(value, str)
5553
}
5654
)
5755

56+
@classmethod
57+
def _convert_value_to_proto(cls, value: Any) -> struct_pb2.Value:
58+
"""Convert Python value to protobuf Value."""
59+
if value is None:
60+
proto_value = struct_pb2.Value()
61+
proto_value.null_value = 0
62+
return proto_value
63+
if isinstance(value, bool):
64+
return struct_pb2.Value(bool_value=value)
65+
if isinstance(value, int):
66+
if abs(value) > (2**53 - 1):
67+
return struct_pb2.Value(string_value=str(value))
68+
return struct_pb2.Value(number_value=float(value))
69+
if isinstance(value, float):
70+
return struct_pb2.Value(number_value=value)
71+
if isinstance(value, str):
72+
return struct_pb2.Value(string_value=value)
73+
if isinstance(value, dict):
74+
serializable_dict = cls._make_json_serializable(value)
75+
json_data = json.dumps(serializable_dict, ensure_ascii=False)
76+
struct_value = struct_pb2.Struct()
77+
json_format.Parse(json_data, struct_value)
78+
return struct_pb2.Value(struct_value=struct_value)
79+
if isinstance(value, list | tuple):
80+
list_value = struct_pb2.ListValue()
81+
for item in value:
82+
converted_item = cls._convert_value_to_proto(item)
83+
list_value.values.append(converted_item)
84+
return struct_pb2.Value(list_value=list_value)
85+
return struct_pb2.Value(string_value=str(value))
86+
87+
@classmethod
88+
def _make_json_serializable(cls, value: Any) -> Any:
89+
if value is None or isinstance(value, str | int | float | bool):
90+
return value
91+
if isinstance(value, dict):
92+
return {k: cls._make_json_serializable(v) for k, v in value.items()}
93+
if isinstance(value, list | tuple):
94+
return [cls._make_json_serializable(item) for item in value]
95+
return str(value)
96+
5897
@classmethod
5998
def part(cls, part: types.Part) -> a2a_pb2.Part:
6099
if isinstance(part.root, types.TextPart):
@@ -478,11 +517,34 @@ def message(cls, message: a2a_pb2.Message) -> types.Message:
478517
@classmethod
479518
def metadata(cls, metadata: struct_pb2.Struct) -> dict[str, Any]:
480519
return {
481-
key: value.string_value
520+
key: cls._convert_proto_to_value(value)
482521
for key, value in metadata.fields.items()
483-
if value.string_value
484522
}
485523

524+
@classmethod
525+
def _convert_proto_to_value(cls, value: struct_pb2.Value) -> Any:
526+
if value.HasField('null_value'):
527+
return None
528+
if value.HasField('bool_value'):
529+
return value.bool_value
530+
if value.HasField('number_value'):
531+
return value.number_value
532+
if value.HasField('string_value'):
533+
string_val = value.string_value
534+
if string_val.lstrip('-').isdigit():
535+
return int(string_val)
536+
return string_val
537+
if value.HasField('struct_value'):
538+
return {
539+
k: cls._convert_proto_to_value(v)
540+
for k, v in value.struct_value.fields.items()
541+
}
542+
if value.HasField('list_value'):
543+
return [
544+
cls._convert_proto_to_value(v) for v in value.list_value.values
545+
]
546+
return None
547+
486548
@classmethod
487549
def part(cls, part: a2a_pb2.Part) -> types.Part:
488550
if part.HasField('text'):

tests/utils/test_proto_utils.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,3 +255,106 @@ def test_none_handling(self):
255255
assert proto_utils.ToProto.provider(None) is None
256256
assert proto_utils.ToProto.security(None) is None
257257
assert proto_utils.ToProto.security_schemes(None) is None
258+
259+
def test_metadata_conversion(self):
260+
"""Test metadata conversion with various data types."""
261+
metadata = {
262+
'null_value': None,
263+
'bool_value': True,
264+
'int_value': 42,
265+
'float_value': 3.14,
266+
'string_value': 'hello',
267+
'dict_value': {'nested': 'dict', 'count': 10},
268+
'list_value': [1, 'two', 3.0, True, None],
269+
'tuple_value': (1, 2, 3),
270+
'complex_list': [
271+
{'name': 'item1', 'values': [1, 2, 3]},
272+
{'name': 'item2', 'values': [4, 5, 6]},
273+
],
274+
}
275+
276+
# Convert to proto
277+
proto_metadata = proto_utils.ToProto.metadata(metadata)
278+
assert proto_metadata is not None
279+
280+
# Convert back to Python
281+
roundtrip_metadata = proto_utils.FromProto.metadata(proto_metadata)
282+
283+
# Verify all values are preserved correctly
284+
assert roundtrip_metadata['null_value'] is None
285+
assert roundtrip_metadata['bool_value'] is True
286+
assert roundtrip_metadata['int_value'] == 42
287+
assert roundtrip_metadata['float_value'] == 3.14
288+
assert roundtrip_metadata['string_value'] == 'hello'
289+
assert roundtrip_metadata['dict_value']['nested'] == 'dict'
290+
assert roundtrip_metadata['dict_value']['count'] == 10
291+
assert roundtrip_metadata['list_value'] == [1, 'two', 3.0, True, None]
292+
assert roundtrip_metadata['tuple_value'] == [
293+
1,
294+
2,
295+
3,
296+
] # tuples become lists
297+
assert len(roundtrip_metadata['complex_list']) == 2
298+
assert roundtrip_metadata['complex_list'][0]['name'] == 'item1'
299+
300+
def test_metadata_with_custom_objects(self):
301+
"""Test metadata conversion with custom objects that need str() fallback."""
302+
303+
class CustomObject:
304+
def __str__(self):
305+
return 'custom_object_str'
306+
307+
def __repr__(self):
308+
return 'CustomObject()'
309+
310+
metadata = {
311+
'custom_obj': CustomObject(),
312+
'list_with_custom': [1, CustomObject(), 'text'],
313+
'nested_custom': {'obj': CustomObject(), 'normal': 'value'},
314+
}
315+
316+
# Convert to proto
317+
proto_metadata = proto_utils.ToProto.metadata(metadata)
318+
assert proto_metadata is not None
319+
320+
# Convert back to Python
321+
roundtrip_metadata = proto_utils.FromProto.metadata(proto_metadata)
322+
323+
# Custom objects should be converted to strings
324+
assert roundtrip_metadata['custom_obj'] == 'custom_object_str'
325+
assert roundtrip_metadata['list_with_custom'] == [
326+
1,
327+
'custom_object_str',
328+
'text',
329+
]
330+
assert roundtrip_metadata['nested_custom']['obj'] == 'custom_object_str'
331+
assert roundtrip_metadata['nested_custom']['normal'] == 'value'
332+
333+
def test_metadata_edge_cases(self):
334+
"""Test metadata conversion with edge cases."""
335+
metadata = {
336+
'empty_dict': {},
337+
'empty_list': [],
338+
'zero': 0,
339+
'false': False,
340+
'empty_string': '',
341+
'unicode_string': 'string test',
342+
'large_number': 9999999999999999,
343+
'negative_number': -42,
344+
'float_precision': 0.123456789,
345+
}
346+
347+
# Convert to proto and back
348+
proto_metadata = proto_utils.ToProto.metadata(metadata)
349+
roundtrip_metadata = proto_utils.FromProto.metadata(proto_metadata)
350+
351+
# Verify edge cases are handled correctly
352+
assert roundtrip_metadata['empty_dict'] == {}
353+
assert roundtrip_metadata['empty_list'] == []
354+
assert roundtrip_metadata['zero'] == 0
355+
assert roundtrip_metadata['false'] is False
356+
assert roundtrip_metadata['empty_string'] == ''
357+
assert roundtrip_metadata['unicode_string'] == 'string test'
358+
assert roundtrip_metadata['large_number'] == 9999999999999999
359+
assert roundtrip_metadata['negative_number'] == -42
360+
assert abs(roundtrip_metadata['float_precision'] - 0.123456789) < 1e-10

0 commit comments

Comments
 (0)