[Bug]: NullPointerException in A2ACommonFieldMapper when a struct field has an explicit null value
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 78/100
Research direction
Start in A2ACommonFieldMapper.structToMap and valueToObject, then trace the TaskMapperImpl/MessageMapperImpl path reached by Client.getTask(...) over JSON-RPC. Done means a response containing nested explicit null fields is converted without a NullPointerException and the null fields remain represented in the resulting data.
Written by the indexing model from the issue text.
Description
What happened?
A2ACommonFieldMapper.structToMap throws a NullPointerException when converting a protobuf Struct that contains a field whose value is an explicit JSON null (protobuf Value.KindCase.NULL_VALUE). This reproduces on 1.3.0.Final when deserializing a Task returned from GetTask, specifically when a message part's structured data contains a null field anywhere in its (possibly nested) object.
This looks like an incomplete fix of #618: that issue fixed the empty-struct case (valueToObject now short-circuits STRUCT_VALUE with 0 fields to Collections.emptyMap() — see lines 218-224 in the current source), but the explicit-null-field case was not addressed and still NPEs.
Root Cause
valueToObject correctly returns Java null for a protobuf NULL_VALUE field:
case NULL_VALUE:
default:
return null;
But structToMap collects the struct's fields with Collectors.toMap:
default Map<String, Object> structToMap(Struct struct) {
if (struct == null || struct.getFieldsCount() == 0) {
return null;
}
return struct.getFieldsMap().entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> valueToObject(e.getValue())));
}
Collectors.toMap's default accumulator calls Map.merge, which calls Objects.requireNonNull(value) — it cannot accept a null mapped value, regardless of how deeply nested the offending field is. Any JSON object with a null field anywhere in a message's structured data part therefore breaks task/message deserialization entirely.
Steps to Reproduce
- Have a server return (via GetTask, or any response going through TaskMapperImpl/MessageMapperImpl) a message part whose structured JSON data contains an explicit null field, e.g.:
{
"data": {
"data": {
"some_optional_field": null,
"other_field": "value"
}
}
}
- Have the client call
Client.getTask(...)(JSON-RPC transport) to fetch that task. - A
NullPointerExceptionis thrown while converting the response, and the client never gets to see the actual task/message content.
Relevant log output
java.lang.NullPointerException
at java.base/java.util.Objects.requireNonNull(Objects.java:233)
at java.base/java.util.stream.Collectors.lambda$uniqKeysMapAccumulator$1(Collectors.java:180)
at java.base/java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
at java.base/java.util.Collections$UnmodifiableMap$UnmodifiableEntrySet.lambda$entryConsumer$0(Collections.java:1778)
at java.base/java.util.Iterator.forEachRemaining(Iterator.java:133)
at java.base/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1939)
at java.base/java.util.Collections$UnmodifiableMap$UnmodifiableEntrySet$UnmodifiableEntrySetSpliterator.forEachRemaining(Collections.java:1803)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682)
at org.a2aproject.sdk.grpc.mapper.A2ACommonFieldMapper.structToMap(A2ACommonFieldMapper.java:170)
at org.a2aproject.sdk.grpc.mapper.A2ACommonFieldMapper.valueToObject(A2ACommonFieldMapper.java:225)
at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1708)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682)
at org.a2aproject.sdk.grpc.mapper.A2ACommonFieldMapper.valueToObject(A2ACommonFieldMapper.java:229)
at org.a2aproject.sdk.grpc.mapper.A2ACommonFieldMapper.lambda$structToMap$1(A2ACommonFieldMapper.java:170)
at java.base/java.util.stream.Collectors.lambda$uniqKeysMapAccumulator$1(Collectors.java:180)
at java.base/java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
at java.base/java.util.Collections$UnmodifiableMap$UnmodifiableEntrySet.lambda$entryConsumer$0(Collections.java:1778)
at java.base/java.util.Iterator.forEachRemaining(Iterator.java:133)
at java.base/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1939)
at java.base/java.util.Collections$UnmodifiableMap$UnmodifiableEntrySet$UnmodifiableEntrySetSpliterator.forEachRemaining(Collections.java:1803)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682)
at org.a2aproject.sdk.grpc.mapper.A2ACommonFieldMapper.structToMap(A2ACommonFieldMapper.java:170)
at org.a2aproject.sdk.grpc.mapper.A2ACommonFieldMapper.valueToObject(A2ACommonFieldMapper.java:225)
at org.a2aproject.sdk.grpc.mapper.A2ACommonFieldMapper.lambda$structToMap$1(A2ACommonFieldMapper.java:170)
at java.base/java.util.stream.Collectors.lambda$uniqKeysMapAccumulator$1(Collectors.java:180)
at java.base/java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
at java.base/java.util.Collections$UnmodifiableMap$UnmodifiableEntrySet.lambda$entryConsumer$0(Collections.java:1778)
at java.base/java.util.Iterator.forEachRemaining(Iterator.java:133)
at java.base/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1939)
at java.base/java.util.Collections$UnmodifiableMap$UnmodifiableEntrySet$UnmodifiableEntrySetSpliterator.forEachRemaining(Collections.java:1803)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682)
at org.a2aproject.sdk.grpc.mapper.A2ACommonFieldMapper.structToMap(A2ACommonFieldMapper.java:170)
at org.a2aproject.sdk.grpc.mapper.A2ACommonFieldMapper.valueToObject(A2ACommonFieldMapper.java:225)
at org.a2aproject.sdk.grpc.mapper.A2ACommonFieldMapper.lambda$structToMap$1(A2ACommonFieldMapper.java:170)
at java.base/java.util.stream.Collectors.lambda$uniqKeysMapAccumulator$1(Collectors.java:180)
at java.base/java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
at java.base/java.util.Collections$UnmodifiableMap$UnmodifiableEntrySet.lambda$entryConsumer$0(Collections.java:1778)
at java.base/java.util.Iterator.forEachRemaining(Iterator.java:133)
at java.base/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1939)
at java.base/java.util.Collections$UnmodifiableMap$UnmodifiableEntrySet$UnmodifiableEntrySetSpliterator.forEachRemaining(Collections.java:1803)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682)
at org.a2aproject.sdk.grpc.mapper.A2ACommonFieldMapper.structToMap(A2ACommonFieldMapper.java:170)
at org.a2aproject.sdk.grpc.mapper.A2ACommonFieldMapper.valueToObject(A2ACommonFieldMapper.java:225)
at org.a2aproject.sdk.grpc.mapper.PartMapper.fromProto(PartMapper.java:110)
at org.a2aproject.sdk.grpc.mapper.MessageMapperImpl.partListToPartArray(MessageMapperImpl.java:88)
at org.a2aproject.sdk.grpc.mapper.MessageMapperImpl.fromProto(MessageMapperImpl.java:72)
at org.a2aproject.sdk.grpc.mapper.TaskMapperImpl.messageListToMessageArray(TaskMapperImpl.java:95)
at org.a2aproject.sdk.grpc.mapper.TaskMapperImpl.fromProto(TaskMapperImpl.java:69)
at org.a2aproject.sdk.grpc.utils.ProtoUtils$FromProto.lambda$task$11(ProtoUtils.java:313)
at org.a2aproject.sdk.grpc.utils.ProtoUtils$FromProto.convert(ProtoUtils.java:198)
at org.a2aproject.sdk.grpc.utils.ProtoUtils$FromProto.task(ProtoUtils.java:313)
at org.a2aproject.sdk.grpc.utils.JSONRPCUtils.parseResponseBody(JSONRPCUtils.java:315)
at org.a2aproject.sdk.client.transport.jsonrpc.JSONRPCTransport.unmarshalResponse(JSONRPCTransport.java:369)
at org.a2aproject.sdk.client.transport.jsonrpc.JSONRPCTransport.getTask(JSONRPCTransport.java:148)
at org.a2aproject.sdk.client.Client.getTask(Client.java:371)
(application code)
Code of Conduct
- I agree to follow this project's Code of Conduct
- Dominant language
- Java
- Stars
- 495
- Forks
- 174
- Avg merge
- 1d 18h
- Merged PRs (30d)
- 48
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from a2aproject/a2a-java
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
a2aproject/a2a-java#1158 · 2 comments ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
a2aproject/a2a-java#1142 · 1 assignee ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
a2aproject/a2a-java#1012 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
a2aproject/a2a-java#464 · 1 comment ·
-
sample
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
a2aproject/a2a-java#375 · 5 comments · 1 assignee ·
All issues in a2aproject/a2a-java
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
infinispan/infinispan#18150 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
-
untriaged
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
opensearch-project/k-NN#3597 ·
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 82/100