From b6e0954c2d24aab94c7cc89bc4dbe8fb78545226 Mon Sep 17 00:00:00 2001 From: quick Date: Mon, 2 Feb 2026 23:06:04 +0800 Subject: [PATCH 1/3] fix: handle empty Struct in valueToObject to prevent NullPointerException When processing a protobuf Value containing an empty Struct, the valueToObject method would call structToMap which returns null for empty structs. This caused a NullPointerException when the result was used in stream collection operations. The fix adds a null/empty check for STRUCT_VALUE case, returning an empty HashMap instead of delegating to structToMap when the struct has no fields. Fixes #618 Co-Authored-By: Claude Opus 4.5 --- .../a2a/grpc/mapper/A2ACommonFieldMapper.java | 3 + .../grpc/mapper/A2ACommonFieldMapperTest.java | 108 ++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 spec-grpc/src/test/java/io/a2a/grpc/mapper/A2ACommonFieldMapperTest.java diff --git a/spec-grpc/src/main/java/io/a2a/grpc/mapper/A2ACommonFieldMapper.java b/spec-grpc/src/main/java/io/a2a/grpc/mapper/A2ACommonFieldMapper.java index 0acdc5d3e..c91e123aa 100644 --- a/spec-grpc/src/main/java/io/a2a/grpc/mapper/A2ACommonFieldMapper.java +++ b/spec-grpc/src/main/java/io/a2a/grpc/mapper/A2ACommonFieldMapper.java @@ -207,6 +207,9 @@ private Value objectToValue(Object value) { private Object valueToObject(Value value) { switch (value.getKindCase()) { case STRUCT_VALUE: + if (value.getStructValue() == null || value.getStructValue().getFieldsCount() < 1) { + return new java.util.HashMap(); + } return structToMap(value.getStructValue()); case LIST_VALUE: return value.getListValue().getValuesList().stream() diff --git a/spec-grpc/src/test/java/io/a2a/grpc/mapper/A2ACommonFieldMapperTest.java b/spec-grpc/src/test/java/io/a2a/grpc/mapper/A2ACommonFieldMapperTest.java new file mode 100644 index 000000000..67b2a0ffd --- /dev/null +++ b/spec-grpc/src/test/java/io/a2a/grpc/mapper/A2ACommonFieldMapperTest.java @@ -0,0 +1,108 @@ +package io.a2a.grpc.mapper; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.Map; + +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.JsonFormat; + +import io.a2a.grpc.utils.ProtoUtils; +import io.a2a.spec.MessageSendParams; +import org.junit.jupiter.api.Test; + +public class A2ACommonFieldMapperTest { + + /** + * Test that valueToObject handles empty struct correctly without throwing NullPointerException. + * + * This test verifies the fix for the bug where an empty struct in the JSON + * (e.g., "response": {}) would cause a NullPointerException because structToMap + * returns null for empty structs. + */ + @Test + void testValueToObject_WithEmptyStruct_ReturnsEmptyMap() throws InvalidProtocolBufferException { + // JSON containing an empty struct in "response" field + String json = "{\n" + + " \"message\": {\n" + + " \"messageId\": \"b3b1ab58-c3d0-4e6d-9e47-9d8a12fe0809\",\n" + + " \"role\": \"ROLE_USER\",\n" + + " \"parts\": [{\n" + + " \"text\": \"Hello\"\n" + + " }, {\n" + + " \"data\": {\n" + + " \"data\": {\n" + + " \"id\": \"call_94yo5ymj3qi5glbpkw5eicfd\",\n" + + " \"args\": {\n" + + " \"agent_name\": \"Default Agent\"\n" + + " },\n" + + " \"name\": \"transfer_to_agent\"\n" + + " }\n" + + " }\n" + + " }, {\n" + + " \"data\": {\n" + + " \"data\": {\n" + + " \"response\": {\n" + + " },\n" + + " \"id\": \"call_94yo5ymj3qi5glbpkw5eicfd\",\n" + + " \"name\": \"transfer_to_agent\"\n" + + " }\n" + + " }\n" + + " }, {\n" + + " \"text\": \"World\"\n" + + " }],\n" + + " \"metadata\": {\n" + + " }\n" + + " },\n" + + " \"configuration\": {\n" + + " \"blocking\": true\n" + + " },\n" + + " \"metadata\": {\n" + + " }\n" + + "}"; + + io.a2a.grpc.SendMessageRequest.Builder builder = io.a2a.grpc.SendMessageRequest.newBuilder(); + JsonFormat.parser().merge(json, builder); + + // This should not throw NullPointerException + MessageSendParams messageSendParams = ProtoUtils.FromProto.messageSendParams(builder); + + assertNotNull(messageSendParams); + assertNotNull(messageSendParams.message()); + assertEquals(4, messageSendParams.message().parts().size()); + } + + /** + * Test that valueToObject handles nested empty struct correctly. + */ + @Test + void testValueToObject_WithNestedEmptyStruct_ReturnsEmptyMap() throws InvalidProtocolBufferException { + String json = "{\n" + + " \"message\": {\n" + + " \"messageId\": \"test-id\",\n" + + " \"role\": \"ROLE_USER\",\n" + + " \"parts\": [{\n" + + " \"data\": {\n" + + " \"data\": {\n" + + " \"nested\": {\n" + + " \"empty\": {\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }]\n" + + " }\n" + + "}"; + + io.a2a.grpc.SendMessageRequest.Builder builder = io.a2a.grpc.SendMessageRequest.newBuilder(); + JsonFormat.parser().merge(json, builder); + + // This should not throw NullPointerException + MessageSendParams messageSendParams = ProtoUtils.FromProto.messageSendParams(builder); + + assertNotNull(messageSendParams); + assertNotNull(messageSendParams.message()); + } +} From d91a69f303234d49fda396b04dd4a5a563671e7b Mon Sep 17 00:00:00 2001 From: LiZongbo Date: Tue, 3 Feb 2026 22:59:48 +0800 Subject: [PATCH 2/3] Update spec-grpc/src/main/java/io/a2a/grpc/mapper/A2ACommonFieldMapper.java Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../main/java/io/a2a/grpc/mapper/A2ACommonFieldMapper.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec-grpc/src/main/java/io/a2a/grpc/mapper/A2ACommonFieldMapper.java b/spec-grpc/src/main/java/io/a2a/grpc/mapper/A2ACommonFieldMapper.java index c91e123aa..3a20b2cbb 100644 --- a/spec-grpc/src/main/java/io/a2a/grpc/mapper/A2ACommonFieldMapper.java +++ b/spec-grpc/src/main/java/io/a2a/grpc/mapper/A2ACommonFieldMapper.java @@ -207,8 +207,8 @@ private Value objectToValue(Object value) { private Object valueToObject(Value value) { switch (value.getKindCase()) { case STRUCT_VALUE: - if (value.getStructValue() == null || value.getStructValue().getFieldsCount() < 1) { - return new java.util.HashMap(); + if (value.getStructValue() == null || value.getStructValue().getFieldsCount() == 0) { + return java.util.Collections.emptyMap(); } return structToMap(value.getStructValue()); case LIST_VALUE: From c09126446c32e1cbe6168e54ffad597c17fa7949 Mon Sep 17 00:00:00 2001 From: quick Date: Sat, 7 Feb 2026 23:57:50 +0800 Subject: [PATCH 3/3] fix: distinguish null Struct from empty Struct in valueToObject Separate the null check from the empty fields check to return null for null Struct values instead of an empty map, preventing incorrect type coercion. Co-Authored-By: Claude Opus 4.5 --- .../main/java/io/a2a/grpc/mapper/A2ACommonFieldMapper.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spec-grpc/src/main/java/io/a2a/grpc/mapper/A2ACommonFieldMapper.java b/spec-grpc/src/main/java/io/a2a/grpc/mapper/A2ACommonFieldMapper.java index 3a20b2cbb..092941704 100644 --- a/spec-grpc/src/main/java/io/a2a/grpc/mapper/A2ACommonFieldMapper.java +++ b/spec-grpc/src/main/java/io/a2a/grpc/mapper/A2ACommonFieldMapper.java @@ -207,7 +207,10 @@ private Value objectToValue(Object value) { private Object valueToObject(Value value) { switch (value.getKindCase()) { case STRUCT_VALUE: - if (value.getStructValue() == null || value.getStructValue().getFieldsCount() == 0) { + if (value.getStructValue() == null) { + return null; + } + if (value.getStructValue().getFieldsCount() == 0) { return java.util.Collections.emptyMap(); } return structToMap(value.getStructValue());