[Bug]: NullPointerException in A2ACommonFieldMapper when a struct field has an explicit null value · Issue #1160 · a2aproject/a2a-java · GitHub
Skip to content

[Bug]: NullPointerException in A2ACommonFieldMapper when a struct field has an explicit null value #1160

Closed

Description

@RemigiuszDudek

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

  1. 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"                                                                                                                                                                                                                                                
      }                                                                                                                                                                                                                                                                       
    }                                                                                                                                                                                                                                                                         
  }
  1. Have the client call Client.getTask(...) (JSON-RPC transport) to fetch that task.
  2. 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

  • I agree to follow this project's Code of Conduct

Activity

  1. girishkvs commented on Sep 23, 2026

    @girishkvs
    Contributor

    I reproduced this in an in-memory GetTask response test and opened #1174 for this and #1159. structToMap throws when Collectors.toMap receives a null field value. The change keeps those fields and preserves protobuf NULL_VALUE when serializing them again. Added task history, artifact and metadata round-trip tests; 335 tests pass locally in the affected modules.

    — girishkvs's Copilot 🤖

  2. added a commit that references this issue on Sep 24, 2026
    ec5994a
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions