diff --git a/docs/migrations/v1_0/README.md b/docs/migrations/v1_0/README.md index 0de221aca..a1af8e7e4 100644 --- a/docs/migrations/v1_0/README.md +++ b/docs/migrations/v1_0/README.md @@ -99,6 +99,7 @@ Constructing messages is simplified in v1.0. The old API required wrapping conte **Note**: * When using `File (bytes)` in v1.0, data serialization (via base64 encoding) is not required because A2A now uses Protobuf, which handles it automatically. * In v1.0, `Part.DataPart.data` is renamed to `Part.data` and is of type `google.protobuf.Value`. Use `ParseDict` to convert a Python dict into a suitable value. See the examples below for more details. +* `google.protobuf.Value` stores every number as a double, so whole numbers in a data Part are read back as floats on every transport (`4326` becomes `4326.0`). This is a fidelity loss, not a spec violation, since JSON does not distinguish the two. On the receiving side, narrow floats for fields you model as integers (for example with `float.is_integer()`) and leave other fields alone. **Before (v0.3):** ```python diff --git a/src/a2a/helpers/proto_helpers.py b/src/a2a/helpers/proto_helpers.py index d7c3ab8cb..bcfded761 100644 --- a/src/a2a/helpers/proto_helpers.py +++ b/src/a2a/helpers/proto_helpers.py @@ -339,6 +339,11 @@ def new_data_part( ) -> Part: """Creates a Part with structured data (google.protobuf.Value). + Note: + ``google.protobuf.Value`` stores every number as a double, so whole + numbers lose their integer type here and are read back as floats + (``4326`` becomes ``4326.0``). See ``get_data_parts``. + Args: data: JSON-serializable data to embed (dict, list, str, etc.). media_type: Optional MIME type of the part content (e.g., "text/plain", "application/json", "image/png"). @@ -407,6 +412,11 @@ def get_data_parts(parts: Sequence[Part]) -> list[Any]: Each returned element is the Python object obtained by converting the ``google.protobuf.Value`` back via ``MessageToDict``. + Note: + Whole numbers come back as floats (``4326`` becomes ``4326.0``) + because ``google.protobuf.Value`` stores numbers as doubles. For + fields you model as integers, narrow floats with ``is_integer()``. + Args: parts: A sequence of ``Part`` objects. diff --git a/tests/helpers/test_proto_helpers.py b/tests/helpers/test_proto_helpers.py index f8479aa76..a3080a7d7 100644 --- a/tests/helpers/test_proto_helpers.py +++ b/tests/helpers/test_proto_helpers.py @@ -471,6 +471,17 @@ def test_get_data_parts_empty() -> None: assert get_data_parts(parts) == [] +@pytest.mark.xfail( + reason='https://github.com/a2aproject/a2a-python/issues/1328', + strict=True, +) +def test_get_data_parts_keeps_whole_numbers_as_ints() -> None: + result = get_data_parts([new_data_part({'id': 4326, 'items': [240]})]) + assert result == [{'id': 4326, 'items': [240]}] + assert isinstance(result[0]['id'], int) + assert isinstance(result[0]['items'][0], int) + + def test_get_raw_parts() -> None: parts = [ Part(raw=b'\x89PNG'),