Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -1009,8 +1010,9 @@ private static DataPart_v0_3 dataPart(org.a2aproject.sdk.compat03.grpc.DataPartO
if (struct == null || struct.getFieldsCount() == 0) {
return null;
}
return struct.getFieldsMap().entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> value(e.getValue())));
Map<String, Object> map = new HashMap<>();
struct.getFieldsMap().forEach((key, value) -> map.put(key, value(value)));
return map;
}

private static @Nullable Object value(Value value) {
Expand All @@ -1029,7 +1031,7 @@ private static DataPart_v0_3 dataPart(org.a2aproject.sdk.compat03.grpc.DataPartO
return value.getStringValue();
case NULL_VALUE:
default:
throw new InvalidRequestError_v0_3();
return null;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package org.a2aproject.sdk.compat03.grpc.utils;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.Map;

import com.google.protobuf.Struct;
import com.google.protobuf.util.JsonFormat;

import org.a2aproject.sdk.compat03.spec.DataPart_v0_3;
import org.a2aproject.sdk.compat03.spec.Part_v0_3;
import org.junit.jupiter.api.Test;

class FromProtoExplicitNull_v0_3_Test {

private static Struct struct(String json) throws Exception {
Struct.Builder builder = Struct.newBuilder();
JsonFormat.parser().merge(json, builder);
return builder.build();
}

@Test
void partDataPartWithExplicitNullField() throws Exception {
var dataPart = org.a2aproject.sdk.compat03.grpc.DataPart.newBuilder()
.setData(struct("{\"some_optional_field\":null,\"other_field\":\"value\"}"))
.build();
var part = org.a2aproject.sdk.compat03.grpc.Part.newBuilder().setData(dataPart).build();

Part_v0_3<?> result = ProtoUtils_v0_3.FromProto.part(part);

assertTrue(result instanceof DataPart_v0_3);
Map<String, Object> data = ((DataPart_v0_3) result).data();
assertNotNull(data);
assertEquals(2, data.size());
assertTrue(data.containsKey("some_optional_field"));
assertNull(data.get("some_optional_field"));
assertEquals("value", data.get("other_field"));
}

@Test
void partMetadataWithExplicitNullField() throws Exception {
var textPart = org.a2aproject.sdk.compat03.grpc.Part.newBuilder()
.setText("hello")
.setMetadata(struct("{\"nullable\":null,\"source\":\"ID\"}"))
.build();

Part_v0_3<?> result = ProtoUtils_v0_3.FromProto.part(textPart);

Map<String, Object> metadata = ((org.a2aproject.sdk.compat03.spec.TextPart_v0_3) result).metadata();
assertNotNull(metadata);
assertEquals(2, metadata.size());
assertTrue(metadata.containsKey("nullable"));
assertNull(metadata.get("nullable"));
assertEquals("ID", metadata.get("source"));
}

@Test
void partDataPartWithNestedNullAndNullInList() throws Exception {
var dataPart = org.a2aproject.sdk.compat03.grpc.DataPart.newBuilder()
.setData(struct("{\"nested\":{\"optional\":null},\"items\":[null,1]}"))
.build();
var part = org.a2aproject.sdk.compat03.grpc.Part.newBuilder().setData(dataPart).build();

Part_v0_3<?> result = ProtoUtils_v0_3.FromProto.part(part);

Map<String, Object> data = ((DataPart_v0_3) result).data();
assertNotNull(data.get("nested"));
assertNull(((Map<?, ?>) data.get("nested")).get("optional"));
}

@Test
void taskWithDataPartThatHasExplicitNullField() throws Exception {
var message = org.a2aproject.sdk.compat03.grpc.Message.newBuilder()
.setRole(org.a2aproject.sdk.compat03.grpc.Role.ROLE_AGENT)
.setMessageId("message-1")
.addContent(org.a2aproject.sdk.compat03.grpc.Part.newBuilder()
.setData(org.a2aproject.sdk.compat03.grpc.DataPart.newBuilder()
.setData(struct("{\"some_optional_field\":null,\"other_field\":\"value\"}"))))
.build();
var task = org.a2aproject.sdk.compat03.grpc.Task.newBuilder()
.setId("task-1")
.setContextId("context-1")
.setStatus(org.a2aproject.sdk.compat03.grpc.TaskStatus.newBuilder()
.setState(org.a2aproject.sdk.compat03.grpc.TaskState.TASK_STATE_COMPLETED))
.addHistory(message)
.build();

var result = ProtoUtils_v0_3.FromProto.task(task);

assertEquals("task-1", result.id());
assertEquals(1, result.history().size());
var dataPart = (DataPart_v0_3) result.history().get(0).parts().get(0);
assertTrue(dataPart.data().containsKey("some_optional_field"));
assertNull(dataPart.data().get("some_optional_field"));
assertEquals("value", dataPart.data().get("other_field"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.util.List;
import java.util.Map;

import org.a2aproject.sdk.compat03.util.Utils_v0_3;
import org.a2aproject.sdk.util.Assert;

/**
Expand Down Expand Up @@ -82,7 +83,7 @@ public Builder(AgentCard_v0_3 card) {
this.defaultOutputModes = card.defaultOutputModes != null ? new ArrayList<>(card.defaultOutputModes) : null;
this.skills = card.skills != null ? new ArrayList<>(card.skills) : null;
this.supportsAuthenticatedExtendedCard = card.supportsAuthenticatedExtendedCard;
this.securitySchemes = card.securitySchemes != null ? Map.copyOf(card.securitySchemes) : null;
this.securitySchemes = Utils_v0_3.unmodifiableNullableShallowMap(card.securitySchemes);
this.security = card.security != null ? new ArrayList<>(card.security) : null;
this.iconUrl = card.iconUrl;
this.additionalInterfaces = card.additionalInterfaces != null ? new ArrayList<>(card.additionalInterfaces) : null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.util.List;
import java.util.Map;

import org.a2aproject.sdk.compat03.util.Utils_v0_3;
import org.a2aproject.sdk.util.Assert;

/**
Expand Down Expand Up @@ -66,7 +67,7 @@ public Builder parts(Part_v0_3<?>... parts) {
}

public Builder metadata(Map<String, Object> metadata) {
this.metadata = Map.copyOf(metadata);
this.metadata = Utils_v0_3.unmodifiableShallowMap(metadata);
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.Map;

import org.a2aproject.sdk.compat03.util.Utils_v0_3;
import org.a2aproject.sdk.util.Assert;
import org.jspecify.annotations.Nullable;

Expand All @@ -17,8 +18,8 @@ public DataPart_v0_3(Map<String, Object> data, @Nullable Map<String, Object> met
if (kind != Kind.DATA) {
throw new IllegalArgumentException("Invalid DataPart kind: " + kind);
}
this.data = Map.copyOf(data);
this.metadata = metadata == null ? Map.of() : Map.copyOf(metadata);
this.data = Utils_v0_3.unmodifiableShallowMap(data);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we update ToProto.value() to handle nulls too? Now that these maps accept null values, converting them back to protobuf produces KIND_NOT_SET rather than NULL_VALUE. #1174 added an explicit setNullValue(NullValue.NULL_VALUE) for the 1.0 mapper. It would be good to do the same here and add a round-trip test.

this.metadata = metadata == null ? Map.of() : Utils_v0_3.unmodifiableShallowMap(metadata);
this.kind = kind;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.Map;

import org.a2aproject.sdk.compat03.util.Utils_v0_3;
import org.a2aproject.sdk.util.Assert;
import org.jspecify.annotations.Nullable;

Expand All @@ -19,7 +20,7 @@ public FilePart_v0_3 (FileContent_v0_3 file, @Nullable Map<String, Object> metad
throw new IllegalArgumentException("Invalid FilePart kind: " + kind);
}
this.file = file;
this.metadata = metadata == null ? Map.of() : Map.copyOf(metadata);
this.metadata = metadata == null ? Map.of() : Utils_v0_3.unmodifiableShallowMap(metadata);
this.kind = kind;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.util.Map;
import java.util.UUID;

import org.a2aproject.sdk.compat03.util.Utils_v0_3;
import org.a2aproject.sdk.util.Assert;
import org.jspecify.annotations.Nullable;

Expand Down Expand Up @@ -42,7 +43,7 @@ public Message_v0_3(Role role, List<Part_v0_3<?>> parts, @Nullable String messag
this.contextId = contextId;
this.taskId = taskId;
this.referenceTaskIds = referenceTaskIds != null ? List.copyOf(referenceTaskIds) : null;
this.metadata = metadata != null ? Map.copyOf(metadata) : null;
this.metadata = Utils_v0_3.unmodifiableNullableShallowMap(metadata);
this.extensions = extensions != null ? List.copyOf(extensions) : null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.Map;

import org.a2aproject.sdk.compat03.util.Utils_v0_3;
import org.a2aproject.sdk.util.Assert;
import org.jspecify.annotations.Nullable;

Expand Down Expand Up @@ -35,7 +36,7 @@ public TaskArtifactUpdateEvent_v0_3 (String taskId, @Nullable Boolean append, @N
this.lastChunk = lastChunk;
this.artifact = artifact;
this.contextId = contextId;
this.metadata = metadata != null ? Map.copyOf(metadata) : null;
this.metadata = Utils_v0_3.unmodifiableNullableShallowMap(metadata);
}

public TaskArtifactUpdateEvent_v0_3(String taskId, Artifact_v0_3 artifact, String contextId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.google.gson.annotations.SerializedName;
import java.util.Map;

import org.a2aproject.sdk.compat03.util.Utils_v0_3;
import org.a2aproject.sdk.util.Assert;
import org.jspecify.annotations.Nullable;

Expand Down Expand Up @@ -34,7 +35,7 @@ public TaskStatusUpdateEvent_v0_3 (String taskId, TaskStatus_v0_3 status, String
this.status = status;
this.contextId = contextId;
this.isFinal = isFinal;
this.metadata = metadata != null ? Map.copyOf(metadata) : null;
this.metadata = Utils_v0_3.unmodifiableNullableShallowMap(metadata);
}

public TaskStatusUpdateEvent_v0_3(String taskId, TaskStatus_v0_3 status, String contextId, boolean isFinal,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.util.List;
import java.util.Map;

import org.a2aproject.sdk.compat03.util.Utils_v0_3;
import org.a2aproject.sdk.util.Assert;
import org.jspecify.annotations.Nullable;

Expand Down Expand Up @@ -35,7 +36,7 @@ public Task_v0_3 (String id, String contextId, TaskStatus_v0_3 status, List<Arti
this.status = status;
this.artifacts = artifacts != null ? List.copyOf(artifacts) : List.of();
this.history = history != null ? List.copyOf(history) : List.of();
this.metadata = metadata != null ? Map.copyOf(metadata) : null;
this.metadata = Utils_v0_3.unmodifiableNullableShallowMap(metadata);
}

public Task_v0_3(String id, String contextId, TaskStatus_v0_3 status, List<Artifact_v0_3> artifacts,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.Map;

import org.a2aproject.sdk.compat03.util.Utils_v0_3;
import org.a2aproject.sdk.util.Assert;
import org.jspecify.annotations.Nullable;

Expand All @@ -18,7 +19,7 @@ public TextPart_v0_3 (String text, @Nullable Map<String, Object> metadata, Kind
throw new IllegalArgumentException("Invalid TextPart kind: " + kind);
}
this.text = text;
this.metadata = metadata == null ? Map.of() : Map.copyOf(metadata);
this.metadata = metadata == null ? Map.of() : Utils_v0_3.unmodifiableShallowMap(metadata);
this.kind = kind;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package org.a2aproject.sdk.compat03.util;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.a2aproject.sdk.compat03.json.JsonProcessingException_v0_3;
import org.a2aproject.sdk.compat03.json.JsonUtil_v0_3;
Expand Down Expand Up @@ -50,6 +53,35 @@ public static <T> T defaultIfNull(T value, T defaultValue) {
return value;
}

/**
* Creates an unmodifiable shallow copy of a non-null map while preserving null values.
* <p>
* Intended for JSON-like maps such as {@code data}, {@code metadata} and {@code headers},
* where an explicit JSON null is valid protocol content. It intentionally does not use
* {@link Map#copyOf(Map)}, which rejects null values.
*
* @param map the source map, must not be {@code null}
* @param <K> the key type
* @param <V> the value type
* @return an unmodifiable shallow copy preserving null values
*/
public static <K, V> Map<K, V> unmodifiableShallowMap(Map<K, V> map) {
return Collections.unmodifiableMap(new HashMap<>(map));
}

/**
* Creates an unmodifiable shallow copy of a nullable map while preserving null values.
*
* @param map the source map, or {@code null}
* @param <K> the key type
* @param <V> the value type
* @return {@code null} if the source is {@code null}, otherwise an unmodifiable shallow copy
* preserving null values
*/
public static <K, V> Map<K, V> unmodifiableNullableShallowMap(Map<K, V> map) {
return map == null ? null : unmodifiableShallowMap(map);
}

/**
* Appends or updates an artifact in a task based on a {@link TaskArtifactUpdateEvent_v0_3}.
* <p>
Expand Down
Loading