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
NullPointerException is 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
What happened?
A2ACommonFieldMapper.structToMapthrows aNullPointerExceptionwhen 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 anullfield 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
valueToObjectcorrectly returns Java null for a protobuf NULL_VALUE field:But
structToMapcollects the struct's fields with Collectors.toMap:Collectors.toMap's default accumulator callsMap.merge, which callsObjects.requireNonNull(value)— it cannot accept anullmapped 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
{ "data": { "data": { "some_optional_field": null, "other_field": "value" } } }Client.getTask(...)(JSON-RPC transport) to fetch that task.NullPointerExceptionis thrown while converting the response, and the client never gets to see the actual task/message content.Relevant log output
Code of Conduct