diff --git a/boms/sdk/pom.xml b/boms/sdk/pom.xml new file mode 100644 index 000000000..c0b51b73a --- /dev/null +++ b/boms/sdk/pom.xml @@ -0,0 +1,257 @@ + + + 4.0.0 + + + io.github.a2asdk + a2a-java-sdk-parent + 0.4.0.Alpha1-SNAPSHOT + ../../pom.xml + + + a2a-java-sdk-bom + pom + + A2A Java SDK - BOM + Bill of Materials (BOM) for A2A Java SDK core modules and dependencies + + + + + + + + ${project.groupId} + a2a-java-sdk-spec + ${project.version} + + + ${project.groupId} + a2a-java-sdk-spec-grpc + ${project.version} + + + + + ${project.groupId} + a2a-java-sdk-common + ${project.version} + + + ${project.groupId} + a2a-java-sdk-http-client + ${project.version} + + + + + ${project.groupId} + a2a-java-sdk-server-common + ${project.version} + + + + + ${project.groupId} + a2a-java-sdk-microprofile-config + ${project.version} + + + + + ${project.groupId} + a2a-java-sdk-client + ${project.version} + + + ${project.groupId} + a2a-java-sdk-client-transport-spi + ${project.version} + + + ${project.groupId} + a2a-java-sdk-client-transport-jsonrpc + ${project.version} + + + ${project.groupId} + a2a-java-sdk-client-transport-grpc + ${project.version} + + + ${project.groupId} + a2a-java-sdk-client-transport-rest + ${project.version} + + + + + ${project.groupId} + a2a-java-sdk-transport-grpc + ${project.version} + + + ${project.groupId} + a2a-java-sdk-transport-jsonrpc + ${project.version} + + + ${project.groupId} + a2a-java-sdk-transport-rest + ${project.version} + + + + + ${project.groupId} + a2a-java-sdk-tests-server-common + ${project.version} + test + + + ${project.groupId} + a2a-java-sdk-tests-server-common + test-jar + test + ${project.version} + + + ${project.groupId} + a2a-java-sdk-server-common + test-jar + test + ${project.version} + + + + + io.grpc + grpc-bom + ${grpc.version} + pom + import + + + org.slf4j + slf4j-bom + ${slf4j.version} + pom + import + + + + + com.google.protobuf + protobuf-java + ${protobuf-java.version} + + + io.smallrye.reactive + mutiny-zero + ${mutiny-zero.version} + + + + + jakarta.enterprise + jakarta.enterprise.cdi-api + ${jakarta.enterprise.cdi-api.version} + + + jakarta.inject + jakarta.inject-api + ${jakarta.inject.jakarta.inject-api.version} + + + jakarta.json + jakarta.json-api + ${jakarta.json-api.version} + provided + + + jakarta.ws.rs + jakarta.ws.rs-api + ${jakarta.ws.rs-api.version} + provided + + + + + org.jspecify + jspecify + 1.0.0 + provided + + + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + io.rest-assured + rest-assured + ${rest-assured.version} + test + + + org.mockito + mockito-core + ${mockito-core.version} + test + + + org.mock-server + mockserver-netty + ${mockserver.version} + test + + + ch.qos.logback + logback-classic + ${logback.version} + test + + + + + + + + org.apache.maven.plugins + maven-invoker-plugin + 3.8.0 + + ${project.build.directory}/it + ${project.build.directory}/local-repo + src/it/settings.xml + + clean + verify + + false + true + invoker.properties + + + io.github.a2asdk:a2a-java-bom-test-utils:${project.version}:jar + + + + + integration-test + + install + run + + + + + + + + diff --git a/boms/test-utils/pom.xml b/boms/test-utils/pom.xml new file mode 100644 index 000000000..5c0cdff0a --- /dev/null +++ b/boms/test-utils/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + + + io.github.a2asdk + a2a-java-sdk-parent + 0.4.0.Alpha1-SNAPSHOT + ../../pom.xml + + + a2a-java-bom-test-utils + jar + + A2A Java SDK - BOM Test Utilities + Shared utilities for BOM integration tests + + + 17 + 17 + UTF-8 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.1 + + + + diff --git a/client/transport/jsonrpc/pom.xml b/client/transport/jsonrpc/pom.xml index e14025c5b..04e2d096f 100644 --- a/client/transport/jsonrpc/pom.xml +++ b/client/transport/jsonrpc/pom.xml @@ -33,6 +33,20 @@ ${project.groupId} a2a-java-sdk-spec + + ${project.groupId} + a2a-java-sdk-spec-grpc + + + com.google.protobuf + protobuf-java-util + provided + + + com.google.protobuf + protobuf-java + provided + org.junit.jupiter junit-jupiter-api diff --git a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransport.java b/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransport.java index 108c80558..92e6d86b9 100644 --- a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransport.java +++ b/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransport.java @@ -7,9 +7,8 @@ import java.util.Map; import java.util.function.Consumer; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; - +import io.a2a.json.JsonProcessingException; +import io.a2a.json.JsonUtil; import io.a2a.client.http.A2ACardResolver; import io.a2a.client.transport.spi.interceptors.ClientCallContext; import io.a2a.client.transport.spi.interceptors.ClientCallInterceptor; @@ -58,18 +57,16 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; -import io.a2a.util.Utils; - public class JSONRPCTransport implements ClientTransport { - private static final TypeReference SEND_MESSAGE_RESPONSE_REFERENCE = new TypeReference<>() {}; - private static final TypeReference GET_TASK_RESPONSE_REFERENCE = new TypeReference<>() {}; - private static final TypeReference CANCEL_TASK_RESPONSE_REFERENCE = new TypeReference<>() {}; - private static final TypeReference GET_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = new TypeReference<>() {}; - private static final TypeReference SET_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = new TypeReference<>() {}; - private static final TypeReference LIST_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = new TypeReference<>() {}; - private static final TypeReference DELETE_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = new TypeReference<>() {}; - private static final TypeReference GET_AUTHENTICATED_EXTENDED_CARD_RESPONSE_REFERENCE = new TypeReference<>() {}; + private static final Class SEND_MESSAGE_RESPONSE_REFERENCE = SendMessageResponse.class; + private static final Class GET_TASK_RESPONSE_REFERENCE = GetTaskResponse.class; + private static final Class CANCEL_TASK_RESPONSE_REFERENCE = CancelTaskResponse.class; + private static final Class GET_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = GetTaskPushNotificationConfigResponse.class; + private static final Class SET_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = SetTaskPushNotificationConfigResponse.class; + private static final Class LIST_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = ListTaskPushNotificationConfigResponse.class; + private static final Class DELETE_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = DeleteTaskPushNotificationConfigResponse.class; + private static final Class GET_AUTHENTICATED_EXTENDED_CARD_RESPONSE_REFERENCE = GetAuthenticatedExtendedCardResponse.class; private final A2AHttpClient httpClient; private final String agentUrl; @@ -112,7 +109,7 @@ public EventKind sendMessage(MessageSendParams request, ClientCallContext contex return response.getResult(); } catch (A2AClientException e) { throw e; - } catch (IOException | InterruptedException e) { + } catch (IOException | InterruptedException | JsonProcessingException e) { throw new A2AClientException("Failed to send message: " + e, e); } } @@ -147,6 +144,8 @@ public void sendMessageStreaming(MessageSendParams request, Consumer listTaskPushNotificationConfigurations( return response.getResult(); } catch (A2AClientException e) { throw e; - } catch (IOException | InterruptedException e) { + } catch (IOException | InterruptedException | JsonProcessingException e) { throw new A2AClientException("Failed to list task push notification configs: " + e, e); } } @@ -290,7 +289,7 @@ public void deleteTaskPushNotificationConfigurations(DeleteTaskPushNotificationC unmarshalResponse(httpResponseBody, DELETE_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE); } catch (A2AClientException e) { throw e; - } catch (IOException | InterruptedException e) { + } catch (IOException | InterruptedException | JsonProcessingException e) { throw new A2AClientException("Failed to delete task push notification configs: " + e, e); } } @@ -326,6 +325,8 @@ public void resubscribe(TaskIdParams request, Consumer event throw new A2AClientException("Failed to send task resubscription request: " + e, e); } catch (InterruptedException e) { throw new A2AClientException("Task resubscription request timed out: " + e, e); + } catch (JsonProcessingException e) { + throw new A2AClientException("Failed to process JSON for task resubscription request: " + e, e); } } @@ -357,7 +358,7 @@ public AgentCard getAgentCard(ClientCallContext context) throws A2AClientExcepti agentCard = response.getResult(); needsExtendedCard = false; return agentCard; - } catch (IOException | InterruptedException e) { + } catch (IOException | InterruptedException | JsonProcessingException e) { throw new A2AClientException("Failed to get authenticated extended agent card: " + e, e); } } catch(A2AClientError e){ @@ -382,7 +383,7 @@ private PayloadAndHeaders applyInterceptors(String methodName, Object payload, return payloadAndHeaders; } - private String sendPostRequest(PayloadAndHeaders payloadAndHeaders) throws IOException, InterruptedException { + private String sendPostRequest(PayloadAndHeaders payloadAndHeaders) throws IOException, InterruptedException, JsonProcessingException { A2AHttpClient.PostBuilder builder = createPostBuilder(payloadAndHeaders); A2AHttpResponse response = builder.post(); if (!response.success()) { @@ -395,7 +396,7 @@ private A2AHttpClient.PostBuilder createPostBuilder(PayloadAndHeaders payloadAnd A2AHttpClient.PostBuilder postBuilder = httpClient.createPost() .url(agentUrl) .addHeader("Content-Type", "application/json") - .body(Utils.OBJECT_MAPPER.writeValueAsString(payloadAndHeaders.getPayload())); + .body(JsonUtil.toJson(payloadAndHeaders.getPayload())); if (payloadAndHeaders.getHeaders() != null) { for (Map.Entry entry : payloadAndHeaders.getHeaders().entrySet()) { @@ -406,9 +407,9 @@ private A2AHttpClient.PostBuilder createPostBuilder(PayloadAndHeaders payloadAnd return postBuilder; } - private > T unmarshalResponse(String response, TypeReference typeReference) + private > T unmarshalResponse(String response, Class responseClass) throws A2AClientException, JsonProcessingException { - T value = Utils.unmarshalFrom(response, typeReference); + T value = JsonUtil.fromJson(response, responseClass); JSONRPCError error = value.getError(); if (error != null) { throw new A2AClientException(error.getMessage() + (error.getData() != null ? ": " + error.getData() : ""), error); @@ -419,4 +420,4 @@ private > T unmarshalResponse(String response, Type private Map getHttpHeaders(ClientCallContext context) { return context != null ? context.getHeaders() : null; } -} \ No newline at end of file +} diff --git a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/sse/SSEEventListener.java b/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/sse/SSEEventListener.java index ff4b93f3c..7ed6585f9 100644 --- a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/sse/SSEEventListener.java +++ b/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/sse/SSEEventListener.java @@ -1,7 +1,10 @@ package io.a2a.client.transport.jsonrpc.sse; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; +import io.a2a.json.JsonProcessingException; +import io.a2a.json.JsonUtil; import io.a2a.spec.JSONRPCError; import io.a2a.spec.StreamingEventKind; import io.a2a.spec.TaskStatusUpdateEvent; @@ -10,8 +13,6 @@ import java.util.function.Consumer; import java.util.logging.Logger; -import static io.a2a.util.Utils.OBJECT_MAPPER; - public class SSEEventListener { private static final Logger log = Logger.getLogger(SSEEventListener.class.getName()); private final Consumer eventHandler; @@ -26,9 +27,11 @@ public SSEEventListener(Consumer eventHandler, public void onMessage(String message, Future completableFuture) { try { - handleMessage(OBJECT_MAPPER.readTree(message),completableFuture); - } catch (JsonProcessingException e) { + handleMessage(JsonParser.parseString(message).getAsJsonObject(), completableFuture); + } catch (JsonSyntaxException e) { log.warning("Failed to parse JSON message: " + message); + } catch (JsonProcessingException e) { + log.warning("Failed to process JSON message: " + message); } } @@ -57,26 +60,22 @@ public void onComplete() { } } - private void handleMessage(JsonNode jsonNode, Future future) { - try { - if (jsonNode.has("error")) { - JSONRPCError error = OBJECT_MAPPER.treeToValue(jsonNode.get("error"), JSONRPCError.class); - if (errorHandler != null) { - errorHandler.accept(error); - } - } else if (jsonNode.has("result")) { - // result can be a Task, Message, TaskStatusUpdateEvent, or TaskArtifactUpdateEvent - JsonNode result = jsonNode.path("result"); - StreamingEventKind event = OBJECT_MAPPER.treeToValue(result, StreamingEventKind.class); - eventHandler.accept(event); - if (event instanceof TaskStatusUpdateEvent && ((TaskStatusUpdateEvent) event).isFinal()) { - future.cancel(true); // close SSE channel - } - } else { - throw new IllegalArgumentException("Unknown message type"); + private void handleMessage(JsonObject jsonObject, Future future) throws JsonProcessingException { + if (jsonObject.has("error")) { + JSONRPCError error = JsonUtil.fromJson(jsonObject.get("error").toString(), JSONRPCError.class); + if (errorHandler != null) { + errorHandler.accept(error); } - } catch (JsonProcessingException e) { - throw new RuntimeException(e); + } else if (jsonObject.has("result")) { + // result can be a Task, Message, TaskStatusUpdateEvent, or TaskArtifactUpdateEvent + String resultJson = jsonObject.get("result").toString(); + StreamingEventKind event = JsonUtil.fromJson(resultJson, StreamingEventKind.class); + eventHandler.accept(event); + if (event instanceof TaskStatusUpdateEvent && ((TaskStatusUpdateEvent) event).isFinal()) { + future.cancel(true); // close SSE channel + } + } else { + throw new IllegalArgumentException("Unknown message type"); } } diff --git a/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JsonStreamingMessages.java b/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JsonStreamingMessages.java index 909955e81..5930af5ed 100644 --- a/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JsonStreamingMessages.java +++ b/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JsonStreamingMessages.java @@ -89,8 +89,7 @@ public class JsonStreamingMessages { ] } } - } - }"""; + }"""; public static final String STREAMING_ERROR_EVENT = """ data: { diff --git a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestErrorMapper.java b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestErrorMapper.java index 965cc2962..c23eab4a0 100644 --- a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestErrorMapper.java +++ b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestErrorMapper.java @@ -1,10 +1,9 @@ package io.a2a.client.transport.rest; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.google.gson.JsonObject; import io.a2a.client.http.A2AHttpResponse; +import io.a2a.json.JsonProcessingException; +import io.a2a.json.JsonUtil; import io.a2a.spec.A2AClientException; import io.a2a.spec.AuthenticatedExtendedCardNotConfiguredError; import io.a2a.spec.ContentTypeNotSupportedError; @@ -26,8 +25,6 @@ */ public class RestErrorMapper { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().registerModule(new JavaTimeModule()); - public static A2AClientException mapRestError(A2AHttpResponse response) { return RestErrorMapper.mapRestError(response.body(), response.status()); } @@ -35,9 +32,9 @@ public static A2AClientException mapRestError(A2AHttpResponse response) { public static A2AClientException mapRestError(String body, int code) { try { if (body != null && !body.isBlank()) { - JsonNode node = OBJECT_MAPPER.readTree(body); - String className = node.findValue("error").asText(); - String errorMessage = node.findValue("message").asText(); + JsonObject node = JsonUtil.fromJson(body, JsonObject.class); + String className = node.has("error") ? node.get("error").getAsString() : ""; + String errorMessage = node.has("message") ? node.get("message").getAsString() : ""; return mapRestError(className, errorMessage, code); } return mapRestError("", "", code); @@ -50,7 +47,7 @@ public static A2AClientException mapRestError(String body, int code) { public static A2AClientException mapRestError(String className, String errorMessage, int code) { return switch (className) { case "io.a2a.spec.TaskNotFoundError" -> new A2AClientException(errorMessage, new TaskNotFoundError()); - case "io.a2a.spec.AuthenticatedExtendedCardNotConfiguredError" -> new A2AClientException(errorMessage, new AuthenticatedExtendedCardNotConfiguredError()); + case "io.a2a.spec.AuthenticatedExtendedCardNotConfiguredError" -> new A2AClientException(errorMessage, new AuthenticatedExtendedCardNotConfiguredError(null, errorMessage, null)); case "io.a2a.spec.ContentTypeNotSupportedError" -> new A2AClientException(errorMessage, new ContentTypeNotSupportedError(null, null, errorMessage)); case "io.a2a.spec.InternalError" -> new A2AClientException(errorMessage, new InternalError(errorMessage)); case "io.a2a.spec.InvalidAgentResponseError" -> new A2AClientException(errorMessage, new InvalidAgentResponseError(null, null, errorMessage)); diff --git a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransport.java b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransport.java index 5b67d97e0..af2df8df2 100644 --- a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransport.java +++ b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransport.java @@ -2,7 +2,7 @@ import static io.a2a.util.Assert.checkNotNullParam; -import com.fasterxml.jackson.core.JsonProcessingException; +import io.a2a.json.JsonProcessingException; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.MessageOrBuilder; import com.google.protobuf.util.JsonFormat; @@ -20,6 +20,7 @@ import io.a2a.grpc.GetTaskPushNotificationConfigRequest; import io.a2a.grpc.GetTaskRequest; import io.a2a.grpc.ListTaskPushNotificationConfigRequest; +import io.a2a.json.JsonUtil; import io.a2a.spec.TaskPushNotificationConfig; import io.a2a.spec.A2AClientException; import io.a2a.spec.AgentCard; @@ -86,7 +87,7 @@ public EventKind sendMessage(MessageSendParams messageSendParams, @Nullable Clie throw new A2AClientException("Failed to send message, wrong response:" + httpResponseBody); } catch (A2AClientException e) { throw e; - } catch (IOException | InterruptedException e) { + } catch (IOException | InterruptedException | JsonProcessingException e) { throw new A2AClientException("Failed to send message: " + e, e); } } @@ -113,6 +114,8 @@ public void sendMessageStreaming(MessageSendParams messageSendParams, Consumer event throw new A2AClientException("Failed to send streaming message request: " + e, e); } catch (InterruptedException e) { throw new A2AClientException("Send streaming message request timed out: " + e, e); + } catch (JsonProcessingException e) { + throw new A2AClientException("Failed to process JSON for streaming message request: " + e, e); } } @@ -329,10 +334,10 @@ public AgentCard getAgentCard(@Nullable ClientCallContext context) throws A2ACli throw RestErrorMapper.mapRestError(response); } String httpResponseBody = response.body(); - agentCard = Utils.OBJECT_MAPPER.readValue(httpResponseBody, AgentCard.class); + agentCard = JsonUtil.fromJson(httpResponseBody, AgentCard.class); needsExtendedCard = false; return agentCard; - } catch (IOException | InterruptedException e) { + } catch (IOException | InterruptedException | JsonProcessingException e) { throw new A2AClientException("Failed to get authenticated extended agent card: " + e, e); } catch (A2AClientError e) { throw new A2AClientException("Failed to get agent card: " + e, e); @@ -356,7 +361,7 @@ private PayloadAndHeaders applyInterceptors(String methodName, @Nullable Message return payloadAndHeaders; } - private String sendPostRequest(String url, PayloadAndHeaders payloadAndHeaders) throws IOException, InterruptedException { + private String sendPostRequest(String url, PayloadAndHeaders payloadAndHeaders) throws IOException, InterruptedException, JsonProcessingException { A2AHttpClient.PostBuilder builder = createPostBuilder(url, payloadAndHeaders); A2AHttpResponse response = builder.post(); if (!response.success()) { diff --git a/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldClient.java b/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldClient.java index a82438a35..17ce1aef7 100644 --- a/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldClient.java +++ b/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldClient.java @@ -8,7 +8,6 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; -import com.fasterxml.jackson.databind.ObjectMapper; import io.a2a.A2A; import io.a2a.client.Client; @@ -18,6 +17,7 @@ import io.a2a.client.http.A2ACardResolver; import io.a2a.client.transport.jsonrpc.JSONRPCTransport; import io.a2a.client.transport.jsonrpc.JSONRPCTransportConfig; +import io.a2a.json.JsonUtil; import io.a2a.spec.AgentCard; import io.a2a.spec.Message; import io.a2a.spec.Part; @@ -31,14 +31,13 @@ public class HelloWorldClient { private static final String SERVER_URL = "http://localhost:9999"; private static final String MESSAGE_TEXT = "how much is 10 USD in INR?"; - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); public static void main(String[] args) { try { AgentCard finalAgentCard = null; AgentCard publicAgentCard = new A2ACardResolver("http://localhost:9999").getAgentCard(); System.out.println("Successfully fetched public agent card:"); - System.out.println(OBJECT_MAPPER.writeValueAsString(publicAgentCard)); + System.out.println(JsonUtil.toJson(publicAgentCard)); System.out.println("Using public agent card for client initialization (default)."); finalAgentCard = publicAgentCard; @@ -48,7 +47,7 @@ public static void main(String[] args) { authHeaders.put("Authorization", "Bearer dummy-token-for-extended-card"); AgentCard extendedAgentCard = A2A.getAgentCard(SERVER_URL, "/agent/authenticatedExtendedCard", authHeaders); System.out.println("Successfully fetched authenticated extended agent card:"); - System.out.println(OBJECT_MAPPER.writeValueAsString(extendedAgentCard)); + System.out.println(JsonUtil.toJson(extendedAgentCard)); System.out.println("Using AUTHENTICATED EXTENDED agent card for client initialization."); finalAgentCard = extendedAgentCard; } else { diff --git a/examples/helloworld/server/pom.xml b/examples/helloworld/server/pom.xml index 3638d9252..345f927d9 100644 --- a/examples/helloworld/server/pom.xml +++ b/examples/helloworld/server/pom.xml @@ -26,7 +26,7 @@ io.quarkus - quarkus-resteasy-jackson + quarkus-resteasy provided diff --git a/extras/push-notification-config-store-database-jpa/pom.xml b/extras/push-notification-config-store-database-jpa/pom.xml index cec16f4c2..bdbf2f7f0 100644 --- a/extras/push-notification-config-store-database-jpa/pom.xml +++ b/extras/push-notification-config-store-database-jpa/pom.xml @@ -50,7 +50,7 @@ io.quarkus - quarkus-rest-client-jackson + quarkus-rest-client test diff --git a/extras/push-notification-config-store-database-jpa/src/main/java/io/a2a/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStore.java b/extras/push-notification-config-store-database-jpa/src/main/java/io/a2a/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStore.java index f002256cd..835ffea38 100644 --- a/extras/push-notification-config-store-database-jpa/src/main/java/io/a2a/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStore.java +++ b/extras/push-notification-config-store-database-jpa/src/main/java/io/a2a/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStore.java @@ -9,7 +9,7 @@ import jakarta.persistence.PersistenceContext; import jakarta.transaction.Transactional; -import com.fasterxml.jackson.core.JsonProcessingException; +import io.a2a.json.JsonProcessingException; import io.a2a.server.tasks.PushNotificationConfigStore; import io.a2a.spec.PushNotificationConfig; import org.slf4j.Logger; diff --git a/extras/push-notification-config-store-database-jpa/src/main/java/io/a2a/extras/pushnotificationconfigstore/database/jpa/JpaPushNotificationConfig.java b/extras/push-notification-config-store-database-jpa/src/main/java/io/a2a/extras/pushnotificationconfigstore/database/jpa/JpaPushNotificationConfig.java index a55e252b5..225936e47 100644 --- a/extras/push-notification-config-store-database-jpa/src/main/java/io/a2a/extras/pushnotificationconfigstore/database/jpa/JpaPushNotificationConfig.java +++ b/extras/push-notification-config-store-database-jpa/src/main/java/io/a2a/extras/pushnotificationconfigstore/database/jpa/JpaPushNotificationConfig.java @@ -6,9 +6,9 @@ import jakarta.persistence.Table; import jakarta.persistence.Transient; -import com.fasterxml.jackson.core.JsonProcessingException; +import io.a2a.json.JsonProcessingException; +import io.a2a.json.JsonUtil; import io.a2a.spec.PushNotificationConfig; -import io.a2a.util.Utils; @Entity @Table(name = "a2a_push_notification_configs") @@ -46,7 +46,7 @@ public void setConfigJson(String configJson) { public PushNotificationConfig getConfig() throws JsonProcessingException { if (config == null) { - this.config = Utils.unmarshalFrom(configJson, PushNotificationConfig.TYPE_REFERENCE); + this.config = JsonUtil.fromJson(configJson, PushNotificationConfig.class); } return config; } @@ -56,12 +56,12 @@ public void setConfig(PushNotificationConfig config) throws JsonProcessingExcept throw new IllegalArgumentException("Mismatched config id. " + "Expected '" + id.getConfigId() + "'. Got: '" + config.id() + "'"); } - configJson = Utils.OBJECT_MAPPER.writeValueAsString(config); + configJson = JsonUtil.toJson(config); this.config = config; } static JpaPushNotificationConfig createFromConfig(String taskId, PushNotificationConfig config) throws JsonProcessingException { - String json = Utils.OBJECT_MAPPER.writeValueAsString(config); + String json = JsonUtil.toJson(config); JpaPushNotificationConfig jpaPushNotificationConfig = new JpaPushNotificationConfig(new TaskConfigId(taskId, config.id()), json); jpaPushNotificationConfig.config = config; diff --git a/extras/queue-manager-replicated/core/src/main/java/io/a2a/extras/queuemanager/replicated/core/ReplicatedEventQueueItem.java b/extras/queue-manager-replicated/core/src/main/java/io/a2a/extras/queuemanager/replicated/core/ReplicatedEventQueueItem.java index 5eac92d4f..092af8c46 100644 --- a/extras/queue-manager-replicated/core/src/main/java/io/a2a/extras/queuemanager/replicated/core/ReplicatedEventQueueItem.java +++ b/extras/queue-manager-replicated/core/src/main/java/io/a2a/extras/queuemanager/replicated/core/ReplicatedEventQueueItem.java @@ -1,10 +1,5 @@ package io.a2a.extras.queuemanager.replicated.core; -import com.fasterxml.jackson.annotation.JsonGetter; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonSetter; - import io.a2a.server.events.EventQueueItem; import io.a2a.spec.Event; import io.a2a.spec.JSONRPCError; @@ -12,11 +7,7 @@ public class ReplicatedEventQueueItem implements EventQueueItem { private String taskId; - - @JsonInclude(JsonInclude.Include.NON_NULL) private StreamingEventKind event; - - @JsonInclude(JsonInclude.Include.NON_NULL) private JSONRPCError error; private boolean closedEvent; @@ -72,13 +63,10 @@ public void setTaskId(String taskId) { * Get the StreamingEventKind event field (for JSON serialization). * @return the StreamingEventKind event or null */ - @JsonGetter("event") - @JsonInclude(JsonInclude.Include.NON_NULL) public StreamingEventKind getStreamingEvent() { return event; } - @JsonSetter("event") public void setEvent(StreamingEventKind event) { this.event = event; this.error = null; // Clear error when setting event @@ -88,13 +76,10 @@ public void setEvent(StreamingEventKind event) { * Get the JSONRPCError field (for JSON serialization). * @return the JSONRPCError or null */ - @JsonGetter("error") - @JsonInclude(JsonInclude.Include.NON_NULL) public JSONRPCError getErrorObject() { return error; } - @JsonSetter("error") public void setError(JSONRPCError error) { this.error = error; this.event = null; // Clear event when setting error @@ -105,7 +90,6 @@ public void setError(JSONRPCError error) { * This is the method required by the EventQueueItem interface. * @return the event (StreamingEventKind, JSONRPCError, or QueueClosedEvent) or null if none is set */ - @JsonIgnore @Override public Event getEvent() { if (closedEvent) { @@ -121,7 +105,6 @@ public Event getEvent() { * Indicates this is a replicated event (implements EventQueueItem). * @return always true for replicated events */ - @JsonIgnore @Override public boolean isReplicated() { return true; @@ -148,7 +131,6 @@ public boolean hasError() { * For JSON serialization. * @return true if this is a queue closed event */ - @JsonGetter("closedEvent") public boolean isClosedEvent() { return closedEvent; } @@ -157,7 +139,6 @@ public boolean isClosedEvent() { * Set the closed event flag (for JSON deserialization). * @param closedEvent true if this is a queue closed event */ - @JsonSetter("closedEvent") public void setClosedEvent(boolean closedEvent) { this.closedEvent = closedEvent; if (closedEvent) { diff --git a/extras/queue-manager-replicated/core/src/test/java/io/a2a/extras/queuemanager/replicated/core/EventSerializationTest.java b/extras/queue-manager-replicated/core/src/test/java/io/a2a/extras/queuemanager/replicated/core/EventSerializationTest.java index 7e18ca4a8..3f7759153 100644 --- a/extras/queue-manager-replicated/core/src/test/java/io/a2a/extras/queuemanager/replicated/core/EventSerializationTest.java +++ b/extras/queue-manager-replicated/core/src/test/java/io/a2a/extras/queuemanager/replicated/core/EventSerializationTest.java @@ -9,7 +9,7 @@ import java.util.List; -import com.fasterxml.jackson.core.JsonProcessingException; +import io.a2a.json.JsonProcessingException; import io.a2a.server.events.QueueClosedEvent; import io.a2a.spec.Artifact; import io.a2a.spec.Event; @@ -32,7 +32,7 @@ import io.a2a.spec.TaskStatusUpdateEvent; import io.a2a.spec.TextPart; import io.a2a.spec.UnsupportedOperationError; -import io.a2a.util.Utils; +import io.a2a.json.JsonUtil; import org.junit.jupiter.api.Test; /** @@ -52,12 +52,12 @@ public void testTaskSerialization() throws JsonProcessingException { .build(); // Test serialization as Event - String json = Utils.OBJECT_MAPPER.writeValueAsString((Event) originalTask); + String json = JsonUtil.toJson(originalTask); assertTrue(json.contains("\"kind\":\"task\""), "JSON should contain task kind"); assertTrue(json.contains("\"id\":\"test-task-123\""), "JSON should contain task ID"); // Test deserialization back to StreamingEventKind - StreamingEventKind deserializedEvent = Utils.OBJECT_MAPPER.readValue(json, StreamingEventKind.class); + StreamingEventKind deserializedEvent = JsonUtil.fromJson(json, StreamingEventKind.class); assertInstanceOf(Task.class, deserializedEvent, "Should deserialize to Task"); Task deserializedTask = (Task) deserializedEvent; @@ -67,7 +67,7 @@ public void testTaskSerialization() throws JsonProcessingException { assertEquals(originalTask.getStatus().state(), deserializedTask.getStatus().state()); // Test as StreamingEventKind - StreamingEventKind deserializedAsStreaming = Utils.OBJECT_MAPPER.readValue(json, StreamingEventKind.class); + StreamingEventKind deserializedAsStreaming = JsonUtil.fromJson(json, StreamingEventKind.class); assertInstanceOf(Task.class, deserializedAsStreaming, "Should deserialize to Task as StreamingEventKind"); } @@ -83,12 +83,12 @@ public void testMessageSerialization() throws JsonProcessingException { .build(); // Test serialization as Event - String json = Utils.OBJECT_MAPPER.writeValueAsString((Event) originalMessage); + String json = JsonUtil.toJson(originalMessage); assertTrue(json.contains("\"kind\":\"message\""), "JSON should contain message kind"); assertTrue(json.contains("\"taskId\":\"test-task-789\""), "JSON should contain task ID"); // Test deserialization back to StreamingEventKind - StreamingEventKind deserializedEvent = Utils.OBJECT_MAPPER.readValue(json, StreamingEventKind.class); + StreamingEventKind deserializedEvent = JsonUtil.fromJson(json, StreamingEventKind.class); assertInstanceOf(Message.class, deserializedEvent, "Should deserialize to Message"); Message deserializedMessage = (Message) deserializedEvent; @@ -98,7 +98,7 @@ public void testMessageSerialization() throws JsonProcessingException { assertEquals(originalMessage.getParts().size(), deserializedMessage.getParts().size()); // Test as StreamingEventKind - StreamingEventKind deserializedAsStreaming = Utils.OBJECT_MAPPER.readValue(json, StreamingEventKind.class); + StreamingEventKind deserializedAsStreaming = JsonUtil.fromJson(json, StreamingEventKind.class); assertInstanceOf(Message.class, deserializedAsStreaming, "Should deserialize to Message as StreamingEventKind"); } @@ -114,13 +114,13 @@ public void testTaskStatusUpdateEventSerialization() throws JsonProcessingExcept .build(); // Test serialization as Event - String json = Utils.OBJECT_MAPPER.writeValueAsString((Event) originalEvent); + String json = JsonUtil.toJson((Event) originalEvent); assertTrue(json.contains("\"kind\":\"status-update\""), "JSON should contain status-update kind"); assertTrue(json.contains("\"taskId\":\"test-task-abc\""), "JSON should contain task ID"); assertTrue(json.contains("\"final\":true"), "JSON should contain final flag"); // Test deserialization back to StreamingEventKind - StreamingEventKind deserializedEvent = Utils.OBJECT_MAPPER.readValue(json, StreamingEventKind.class); + StreamingEventKind deserializedEvent = JsonUtil.fromJson(json, StreamingEventKind.class); assertInstanceOf(TaskStatusUpdateEvent.class, deserializedEvent, "Should deserialize to TaskStatusUpdateEvent"); TaskStatusUpdateEvent deserializedStatusEvent = (TaskStatusUpdateEvent) deserializedEvent; @@ -131,7 +131,7 @@ public void testTaskStatusUpdateEventSerialization() throws JsonProcessingExcept assertEquals(originalEvent.isFinal(), deserializedStatusEvent.isFinal()); // Test as StreamingEventKind - StreamingEventKind deserializedAsStreaming = Utils.OBJECT_MAPPER.readValue(json, StreamingEventKind.class); + StreamingEventKind deserializedAsStreaming = JsonUtil.fromJson(json, StreamingEventKind.class); assertInstanceOf(TaskStatusUpdateEvent.class, deserializedAsStreaming, "Should deserialize to TaskStatusUpdateEvent as StreamingEventKind"); } @@ -147,13 +147,13 @@ public void testTaskArtifactUpdateEventSerialization() throws JsonProcessingExce .build(); // Test serialization as Event - String json = Utils.OBJECT_MAPPER.writeValueAsString((Event) originalEvent); + String json = JsonUtil.toJson((Event) originalEvent); assertTrue(json.contains("\"kind\":\"artifact-update\""), "JSON should contain artifact-update kind"); assertTrue(json.contains("\"taskId\":\"test-task-xyz\""), "JSON should contain task ID"); assertTrue(json.contains("\"test-artifact-123\""), "JSON should contain artifact ID"); // Test deserialization back to StreamingEventKind - StreamingEventKind deserializedEvent = Utils.OBJECT_MAPPER.readValue(json, StreamingEventKind.class); + StreamingEventKind deserializedEvent = JsonUtil.fromJson(json, StreamingEventKind.class); assertInstanceOf(TaskArtifactUpdateEvent.class, deserializedEvent, "Should deserialize to TaskArtifactUpdateEvent"); TaskArtifactUpdateEvent deserializedArtifactEvent = (TaskArtifactUpdateEvent) deserializedEvent; @@ -164,7 +164,7 @@ public void testTaskArtifactUpdateEventSerialization() throws JsonProcessingExce assertEquals(originalEvent.getArtifact().name(), deserializedArtifactEvent.getArtifact().name()); // Test as StreamingEventKind - StreamingEventKind deserializedAsStreaming = Utils.OBJECT_MAPPER.readValue(json, StreamingEventKind.class); + StreamingEventKind deserializedAsStreaming = JsonUtil.fromJson(json, StreamingEventKind.class); assertInstanceOf(TaskArtifactUpdateEvent.class, deserializedAsStreaming, "Should deserialize to TaskArtifactUpdateEvent as StreamingEventKind"); } @@ -186,11 +186,11 @@ public void testJSONRPCErrorSubclassesSerialization() throws JsonProcessingExcep for (JSONRPCError originalError : errors) { // Test serialization - String json = Utils.OBJECT_MAPPER.writeValueAsString(originalError); + String json = JsonUtil.toJson(originalError); assertTrue(json.contains("\"message\""), "JSON should contain error message for " + originalError.getClass().getSimpleName()); // Test deserialization - it's acceptable to deserialize as base JSONRPCError - JSONRPCError deserializedError = Utils.OBJECT_MAPPER.readValue(json, JSONRPCError.class); + JSONRPCError deserializedError = JsonUtil.fromJson(json, JSONRPCError.class); assertNotNull(deserializedError, "Should deserialize successfully for " + originalError.getClass().getSimpleName()); assertEquals(originalError.getMessage(), deserializedError.getMessage(), "Error message should match for " + originalError.getClass().getSimpleName()); assertEquals(originalError.getCode(), deserializedError.getCode(), "Error code should match for " + originalError.getClass().getSimpleName()); @@ -213,14 +213,14 @@ public void testReplicatedEventWithStreamingEventSerialization() throws JsonProc ReplicatedEventQueueItem originalReplicatedEvent = new ReplicatedEventQueueItem("replicated-test-task", statusEvent); // Serialize the ReplicatedEventQueueItem - String json = Utils.OBJECT_MAPPER.writeValueAsString(originalReplicatedEvent); + String json = JsonUtil.toJson(originalReplicatedEvent); assertTrue(json.contains("\"taskId\":\"replicated-test-task\""), "JSON should contain task ID"); assertTrue(json.contains("\"event\""), "JSON should contain event field"); assertTrue(json.contains("\"kind\":\"status-update\""), "JSON should contain the event kind"); assertFalse(json.contains("\"error\""), "JSON should not contain error field"); // Deserialize the ReplicatedEventQueueItem - ReplicatedEventQueueItem deserializedReplicatedEvent = Utils.OBJECT_MAPPER.readValue(json, ReplicatedEventQueueItem.class); + ReplicatedEventQueueItem deserializedReplicatedEvent = JsonUtil.fromJson(json, ReplicatedEventQueueItem.class); assertEquals(originalReplicatedEvent.getTaskId(), deserializedReplicatedEvent.getTaskId()); // Now we should get the proper type back! @@ -250,14 +250,14 @@ public void testReplicatedEventWithErrorSerialization() throws JsonProcessingExc ReplicatedEventQueueItem originalReplicatedEvent = new ReplicatedEventQueueItem("error-test-task", error); // Serialize the ReplicatedEventQueueItemQueueItem - String json = Utils.OBJECT_MAPPER.writeValueAsString(originalReplicatedEvent); + String json = JsonUtil.toJson(originalReplicatedEvent); assertTrue(json.contains("\"taskId\":\"error-test-task\""), "JSON should contain task ID"); assertTrue(json.contains("\"error\""), "JSON should contain error field"); assertTrue(json.contains("\"message\""), "JSON should contain error message"); assertFalse(json.contains("\"event\""), "JSON should not contain event field"); // Deserialize the ReplicatedEventQueueItem - ReplicatedEventQueueItem deserializedReplicatedEvent = Utils.OBJECT_MAPPER.readValue(json, ReplicatedEventQueueItem.class); + ReplicatedEventQueueItem deserializedReplicatedEvent = JsonUtil.fromJson(json, ReplicatedEventQueueItem.class); assertEquals(originalReplicatedEvent.getTaskId(), deserializedReplicatedEvent.getTaskId()); // Should get the error back @@ -308,14 +308,14 @@ public void testQueueClosedEventSerialization() throws JsonProcessingException { assertFalse(originalReplicatedEvent.hasError(), "Should not have error"); // Serialize the ReplicatedEventQueueItem - String json = Utils.OBJECT_MAPPER.writeValueAsString(originalReplicatedEvent); + String json = JsonUtil.toJson(originalReplicatedEvent); assertTrue(json.contains("\"taskId\":\"" + taskId + "\""), "JSON should contain task ID"); assertTrue(json.contains("\"closedEvent\":true"), "JSON should contain closedEvent flag set to true"); assertFalse(json.contains("\"event\""), "JSON should not contain event field"); assertFalse(json.contains("\"error\""), "JSON should not contain error field"); // Deserialize the ReplicatedEventQueueItem - ReplicatedEventQueueItem deserializedReplicatedEvent = Utils.OBJECT_MAPPER.readValue(json, ReplicatedEventQueueItem.class); + ReplicatedEventQueueItem deserializedReplicatedEvent = JsonUtil.fromJson(json, ReplicatedEventQueueItem.class); assertEquals(taskId, deserializedReplicatedEvent.getTaskId()); // Verify the deserialized item is marked as a closed event diff --git a/extras/queue-manager-replicated/core/src/test/java/io/a2a/extras/queuemanager/replicated/core/ReplicatedQueueManagerTest.java b/extras/queue-manager-replicated/core/src/test/java/io/a2a/extras/queuemanager/replicated/core/ReplicatedQueueManagerTest.java index 3e2da2f51..42454ea3b 100644 --- a/extras/queue-manager-replicated/core/src/test/java/io/a2a/extras/queuemanager/replicated/core/ReplicatedQueueManagerTest.java +++ b/extras/queue-manager-replicated/core/src/test/java/io/a2a/extras/queuemanager/replicated/core/ReplicatedQueueManagerTest.java @@ -27,7 +27,7 @@ import io.a2a.spec.TaskState; import io.a2a.spec.TaskStatus; import io.a2a.spec.TaskStatusUpdateEvent; -import io.a2a.util.Utils; +import io.a2a.json.JsonUtil; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -199,14 +199,14 @@ void testReplicatedEventJsonSerialization() throws Exception { ReplicatedEventQueueItem original = new ReplicatedEventQueueItem("json-test-task", originalEvent); // Serialize to JSON - String json = Utils.OBJECT_MAPPER.writeValueAsString(original); + String json = JsonUtil.toJson(original); assertNotNull(json); assertTrue(json.contains("json-test-task")); assertTrue(json.contains("\"event\":{")); assertTrue(json.contains("\"kind\":\"status-update\"")); // Deserialize back - ReplicatedEventQueueItem deserialized = Utils.OBJECT_MAPPER.readValue(json, ReplicatedEventQueueItem.class); + ReplicatedEventQueueItem deserialized = JsonUtil.fromJson(json, ReplicatedEventQueueItem.class); assertNotNull(deserialized); assertEquals("json-test-task", deserialized.getTaskId()); assertNotNull(deserialized.getEvent()); @@ -427,13 +427,13 @@ void testQueueClosedEventJsonSerialization() throws Exception { assertFalse(original.hasError(), "Should not have error"); // Serialize to JSON - String json = Utils.OBJECT_MAPPER.writeValueAsString(original); + String json = JsonUtil.toJson(original); assertNotNull(json); assertTrue(json.contains(taskId), "JSON should contain taskId"); assertTrue(json.contains("\"closedEvent\":true"), "JSON should contain closedEvent flag"); // Deserialize back - ReplicatedEventQueueItem deserialized = Utils.OBJECT_MAPPER.readValue(json, ReplicatedEventQueueItem.class); + ReplicatedEventQueueItem deserialized = JsonUtil.fromJson(json, ReplicatedEventQueueItem.class); assertNotNull(deserialized); assertEquals(taskId, deserialized.getTaskId()); assertTrue(deserialized.isClosedEvent(), "Deserialized should be marked as closed event"); diff --git a/extras/queue-manager-replicated/replication-mp-reactive/src/main/java/io/a2a/extras/queuemanager/replicated/mp_reactive/ReactiveMessagingReplicationStrategy.java b/extras/queue-manager-replicated/replication-mp-reactive/src/main/java/io/a2a/extras/queuemanager/replicated/mp_reactive/ReactiveMessagingReplicationStrategy.java index 04238c27d..26797bc5f 100644 --- a/extras/queue-manager-replicated/replication-mp-reactive/src/main/java/io/a2a/extras/queuemanager/replicated/mp_reactive/ReactiveMessagingReplicationStrategy.java +++ b/extras/queue-manager-replicated/replication-mp-reactive/src/main/java/io/a2a/extras/queuemanager/replicated/mp_reactive/ReactiveMessagingReplicationStrategy.java @@ -4,10 +4,9 @@ import jakarta.enterprise.event.Event; import jakarta.inject.Inject; -import com.fasterxml.jackson.core.type.TypeReference; import io.a2a.extras.queuemanager.replicated.core.ReplicatedEventQueueItem; import io.a2a.extras.queuemanager.replicated.core.ReplicationStrategy; -import io.a2a.util.Utils; +import io.a2a.json.JsonUtil; import org.eclipse.microprofile.reactive.messaging.Channel; import org.eclipse.microprofile.reactive.messaging.Emitter; import org.eclipse.microprofile.reactive.messaging.Incoming; @@ -18,7 +17,6 @@ public class ReactiveMessagingReplicationStrategy implements ReplicationStrategy { private static final Logger LOGGER = LoggerFactory.getLogger(ReactiveMessagingReplicationStrategy.class); - private static final TypeReference REPLICATED_EVENT_TYPE_REF = new TypeReference() {}; @Inject @Channel("replicated-events-out") @@ -33,7 +31,7 @@ public void send(String taskId, io.a2a.spec.Event event) { try { ReplicatedEventQueueItem replicatedEvent = new ReplicatedEventQueueItem(taskId, event); - String json = Utils.OBJECT_MAPPER.writeValueAsString(replicatedEvent); + String json = JsonUtil.toJson(replicatedEvent); emitter.send(json); LOGGER.debug("Successfully sent replicated event for task: {}", taskId); } catch (Exception e) { @@ -47,7 +45,7 @@ public void onReplicatedEvent(String jsonMessage) { LOGGER.debug("Received replicated event JSON: {}", jsonMessage); try { - ReplicatedEventQueueItem replicatedEvent = Utils.unmarshalFrom(jsonMessage, REPLICATED_EVENT_TYPE_REF); + ReplicatedEventQueueItem replicatedEvent = JsonUtil.fromJson(jsonMessage, ReplicatedEventQueueItem.class); LOGGER.debug("Deserialized replicated event for task: {}, event: {}", replicatedEvent.getTaskId(), replicatedEvent.getEvent()); diff --git a/extras/queue-manager-replicated/replication-mp-reactive/src/test/java/io/a2a/extras/queuemanager/replicated/mp_reactive/ReactiveMessagingReplicationStrategyTest.java b/extras/queue-manager-replicated/replication-mp-reactive/src/test/java/io/a2a/extras/queuemanager/replicated/mp_reactive/ReactiveMessagingReplicationStrategyTest.java index a10b019db..26976d1e3 100644 --- a/extras/queue-manager-replicated/replication-mp-reactive/src/test/java/io/a2a/extras/queuemanager/replicated/mp_reactive/ReactiveMessagingReplicationStrategyTest.java +++ b/extras/queue-manager-replicated/replication-mp-reactive/src/test/java/io/a2a/extras/queuemanager/replicated/mp_reactive/ReactiveMessagingReplicationStrategyTest.java @@ -19,7 +19,7 @@ import io.a2a.spec.TaskStatus; import io.a2a.spec.TaskState; import io.a2a.spec.TaskStatusUpdateEvent; -import io.a2a.util.Utils; +import io.a2a.json.JsonUtil; @ExtendWith(MockitoExtension.class) class ReactiveMessagingReplicationStrategyTest { @@ -54,7 +54,7 @@ private String createValidJsonMessage(String taskId, String contextId) throws Ex .isFinal(false) .build(); ReplicatedEventQueueItem replicatedEvent = new ReplicatedEventQueueItem(taskId, event); - return Utils.OBJECT_MAPPER.writeValueAsString(replicatedEvent); + return JsonUtil.toJson(replicatedEvent); } @Test diff --git a/extras/queue-manager-replicated/tests-single-instance/src/test/java/io/a2a/extras/queuemanager/replicated/tests/KafkaReplicationIntegrationTest.java b/extras/queue-manager-replicated/tests-single-instance/src/test/java/io/a2a/extras/queuemanager/replicated/tests/KafkaReplicationIntegrationTest.java index 52f5676d3..08c198281 100644 --- a/extras/queue-manager-replicated/tests-single-instance/src/test/java/io/a2a/extras/queuemanager/replicated/tests/KafkaReplicationIntegrationTest.java +++ b/extras/queue-manager-replicated/tests-single-instance/src/test/java/io/a2a/extras/queuemanager/replicated/tests/KafkaReplicationIntegrationTest.java @@ -39,7 +39,7 @@ import io.a2a.spec.TaskStatus; import io.a2a.spec.TaskStatusUpdateEvent; import io.a2a.spec.TextPart; -import io.a2a.util.Utils; +import io.a2a.json.JsonUtil; import io.quarkus.test.junit.QuarkusTest; import org.eclipse.microprofile.reactive.messaging.Channel; import org.eclipse.microprofile.reactive.messaging.Emitter; @@ -261,7 +261,7 @@ public void testKafkaEventReceivedByA2AServer() throws Exception { .build(); ReplicatedEventQueueItem replicatedEvent = new ReplicatedEventQueueItem(taskId, statusEvent); - String eventJson = Utils.OBJECT_MAPPER.writeValueAsString(replicatedEvent); + String eventJson = JsonUtil.toJson(replicatedEvent); // Send to Kafka using reactive messaging testEmitter.send(eventJson); @@ -361,7 +361,7 @@ public void testQueueClosedEventTerminatesRemoteSubscribers() throws Exception { // Now manually send a QueueClosedEvent to Kafka to simulate queue closure on another node QueueClosedEvent closedEvent = new QueueClosedEvent(taskId); ReplicatedEventQueueItem replicatedClosedEvent = new ReplicatedEventQueueItem(taskId, closedEvent); - String eventJson = Utils.OBJECT_MAPPER.writeValueAsString(replicatedClosedEvent); + String eventJson = JsonUtil.toJson(replicatedClosedEvent); // Send to Kafka using reactive messaging testEmitter.send(eventJson); diff --git a/extras/queue-manager-replicated/tests-single-instance/src/test/java/io/a2a/extras/queuemanager/replicated/tests/TestKafkaEventConsumer.java b/extras/queue-manager-replicated/tests-single-instance/src/test/java/io/a2a/extras/queuemanager/replicated/tests/TestKafkaEventConsumer.java index 7571a6ba9..9e308ec42 100644 --- a/extras/queue-manager-replicated/tests-single-instance/src/test/java/io/a2a/extras/queuemanager/replicated/tests/TestKafkaEventConsumer.java +++ b/extras/queue-manager-replicated/tests-single-instance/src/test/java/io/a2a/extras/queuemanager/replicated/tests/TestKafkaEventConsumer.java @@ -9,7 +9,7 @@ import org.eclipse.microprofile.reactive.messaging.Incoming; import io.a2a.extras.queuemanager.replicated.core.ReplicatedEventQueueItem; -import io.a2a.util.Utils; +import io.a2a.json.JsonUtil; import io.quarkus.arc.profile.IfBuildProfile; /** @@ -26,7 +26,7 @@ public class TestKafkaEventConsumer { @Incoming("test-replicated-events-in") public void onTestReplicatedEvent(String jsonMessage) { try { - ReplicatedEventQueueItem event = Utils.OBJECT_MAPPER.readValue(jsonMessage, ReplicatedEventQueueItem.class); + ReplicatedEventQueueItem event = JsonUtil.fromJson(jsonMessage, ReplicatedEventQueueItem.class); receivedEvents.offer(event); // Signal any waiting threads diff --git a/extras/task-store-database-jpa/pom.xml b/extras/task-store-database-jpa/pom.xml index 55a3e619f..c078e3b57 100644 --- a/extras/task-store-database-jpa/pom.xml +++ b/extras/task-store-database-jpa/pom.xml @@ -54,7 +54,7 @@ io.quarkus - quarkus-rest-client-jackson + quarkus-rest-client test diff --git a/extras/task-store-database-jpa/src/main/java/io/a2a/extras/taskstore/database/jpa/JpaDatabaseTaskStore.java b/extras/task-store-database-jpa/src/main/java/io/a2a/extras/taskstore/database/jpa/JpaDatabaseTaskStore.java index edfbfaf69..23fc83759 100644 --- a/extras/task-store-database-jpa/src/main/java/io/a2a/extras/taskstore/database/jpa/JpaDatabaseTaskStore.java +++ b/extras/task-store-database-jpa/src/main/java/io/a2a/extras/taskstore/database/jpa/JpaDatabaseTaskStore.java @@ -13,7 +13,7 @@ import jakarta.persistence.PersistenceContext; import jakarta.transaction.Transactional; -import com.fasterxml.jackson.core.JsonProcessingException; +import io.a2a.json.JsonProcessingException; import io.a2a.extras.common.events.TaskFinalizedEvent; import io.a2a.server.config.A2AConfigProvider; import io.a2a.server.tasks.TaskStateProvider; diff --git a/extras/task-store-database-jpa/src/main/java/io/a2a/extras/taskstore/database/jpa/JpaTask.java b/extras/task-store-database-jpa/src/main/java/io/a2a/extras/taskstore/database/jpa/JpaTask.java index 2b3fc56f1..eb98cc096 100644 --- a/extras/task-store-database-jpa/src/main/java/io/a2a/extras/taskstore/database/jpa/JpaTask.java +++ b/extras/task-store-database-jpa/src/main/java/io/a2a/extras/taskstore/database/jpa/JpaTask.java @@ -8,9 +8,9 @@ import jakarta.persistence.Table; import jakarta.persistence.Transient; -import com.fasterxml.jackson.core.JsonProcessingException; +import io.a2a.json.JsonProcessingException; +import io.a2a.json.JsonUtil; import io.a2a.spec.Task; -import io.a2a.util.Utils; @Entity @Table(name = "a2a_tasks") @@ -76,13 +76,13 @@ public void setFinalizedAt(Instant finalizedAt, boolean isFinalState) { public Task getTask() throws JsonProcessingException { if (task == null) { - this.task = Utils.unmarshalFrom(taskJson, Task.TYPE_REFERENCE); + this.task = JsonUtil.fromJson(taskJson, Task.class); } return task; } public void setTask(Task task) throws JsonProcessingException { - taskJson = Utils.OBJECT_MAPPER.writeValueAsString(task); + taskJson = JsonUtil.toJson(task); if (id == null) { id = task.getId(); } @@ -91,7 +91,7 @@ public void setTask(Task task) throws JsonProcessingException { } static JpaTask createFromTask(Task task) throws JsonProcessingException { - String json = Utils.OBJECT_MAPPER.writeValueAsString(task); + String json = JsonUtil.toJson(task); JpaTask jpaTask = new JpaTask(task.getId(), json); jpaTask.task = task; jpaTask.updateFinalizedTimestamp(task); diff --git a/extras/task-store-database-jpa/src/test/java/io/a2a/extras/taskstore/database/jpa/JpaDatabaseTaskStoreTest.java b/extras/task-store-database-jpa/src/test/java/io/a2a/extras/taskstore/database/jpa/JpaDatabaseTaskStoreTest.java index 8c091f87f..30cf0c75a 100644 --- a/extras/task-store-database-jpa/src/test/java/io/a2a/extras/taskstore/database/jpa/JpaDatabaseTaskStoreTest.java +++ b/extras/task-store-database-jpa/src/test/java/io/a2a/extras/taskstore/database/jpa/JpaDatabaseTaskStoreTest.java @@ -180,7 +180,7 @@ public void testTaskWithComplexMetadata() { assertEquals("test-task-5", retrieved.getId()); assertNotNull(retrieved.getMetadata()); assertEquals("value1", retrieved.getMetadata().get("key1")); - assertEquals(42, retrieved.getMetadata().get("key2")); + assertEquals(42, ((Number)retrieved.getMetadata().get("key2")).intValue()); assertEquals(true, retrieved.getMetadata().get("key3")); } diff --git a/http-client/src/main/java/io/a2a/client/http/A2ACardResolver.java b/http-client/src/main/java/io/a2a/client/http/A2ACardResolver.java index 539f3a741..22af7c615 100644 --- a/http-client/src/main/java/io/a2a/client/http/A2ACardResolver.java +++ b/http-client/src/main/java/io/a2a/client/http/A2ACardResolver.java @@ -1,14 +1,12 @@ package io.a2a.client.http; -import static io.a2a.util.Utils.unmarshalFrom; - import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.Map; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; +import io.a2a.json.JsonProcessingException; +import io.a2a.json.JsonUtil; import io.a2a.spec.A2AClientError; import io.a2a.spec.A2AClientJSONError; import io.a2a.spec.AgentCard; @@ -21,8 +19,6 @@ public class A2ACardResolver { private static final String DEFAULT_AGENT_CARD_PATH = "/.well-known/agent-card.json"; - private static final TypeReference AGENT_CARD_TYPE_REFERENCE = new TypeReference<>() {}; - /** * Get the agent card for an A2A agent. * The {@code JdkA2AHttpClient} will be used to fetch the agent card. @@ -106,7 +102,7 @@ public AgentCard getAgentCard() throws A2AClientError, A2AClientJSONError { } try { - return unmarshalFrom(body, AGENT_CARD_TYPE_REFERENCE); + return JsonUtil.fromJson(body, AgentCard.class); } catch (JsonProcessingException e) { throw new A2AClientJSONError("Could not unmarshal agent card response", e); } diff --git a/http-client/src/test/java/io/a2a/client/http/A2ACardResolverTest.java b/http-client/src/test/java/io/a2a/client/http/A2ACardResolverTest.java index 99d26adad..9c2a177ec 100644 --- a/http-client/src/test/java/io/a2a/client/http/A2ACardResolverTest.java +++ b/http-client/src/test/java/io/a2a/client/http/A2ACardResolverTest.java @@ -1,7 +1,5 @@ package io.a2a.client.http; -import static io.a2a.util.Utils.OBJECT_MAPPER; -import static io.a2a.util.Utils.unmarshalFrom; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -10,7 +8,7 @@ import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; -import com.fasterxml.jackson.core.type.TypeReference; +import io.a2a.json.JsonUtil; import io.a2a.spec.A2AClientError; import io.a2a.spec.A2AClientJSONError; import io.a2a.spec.AgentCard; @@ -20,7 +18,6 @@ public class A2ACardResolverTest { private static final String AGENT_CARD_PATH = "/.well-known/agent-card.json"; - private static final TypeReference AGENT_CARD_TYPE_REFERENCE = new TypeReference<>() {}; @Test public void testConstructorStripsSlashes() throws Exception { @@ -72,10 +69,10 @@ public void testGetAgentCardSuccess() throws Exception { A2ACardResolver resolver = new A2ACardResolver(client, "http://example.com/"); AgentCard card = resolver.getAgentCard(); - AgentCard expectedCard = unmarshalFrom(JsonMessages.AGENT_CARD, AGENT_CARD_TYPE_REFERENCE); - String expected = OBJECT_MAPPER.writeValueAsString(expectedCard); + AgentCard expectedCard = JsonUtil.fromJson(JsonMessages.AGENT_CARD, AgentCard.class); + String expected = JsonUtil.toJson(expectedCard); - String requestCardString = OBJECT_MAPPER.writeValueAsString(card); + String requestCardString = JsonUtil.toJson(card); assertEquals(expected, requestCardString); } diff --git a/pom.xml b/pom.xml index 4d2557a53..83cff974b 100644 --- a/pom.xml +++ b/pom.xml @@ -48,7 +48,7 @@ 3.8.0 3.2.4 0.8.0 - 2.17.0 + 2.13.2 4.1.0 2.0.1 2.1.3 @@ -58,7 +58,7 @@ 5.15.0 1.1.1 1.7.1 - 4.31.1 + 4.33.1 0.6.1 3.28.2 5.5.1 @@ -192,19 +192,25 @@ import - com.fasterxml.jackson.core - jackson-databind - ${jackson.version} + com.google.protobuf + protobuf-java + ${protobuf-java.version} + + + com.google.code.gson + gson + + - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - ${jackson.version} + com.google.protobuf + protobuf-java-util + ${protobuf-java.version} - com.google.protobuf - protobuf-java - ${protobuf.version} + com.google.code.gson + gson + ${gson.version} io.smallrye.reactive @@ -296,7 +302,6 @@ org.jspecify jspecify - diff --git a/reference/common/pom.xml b/reference/common/pom.xml index f779abd4b..47450b9b9 100644 --- a/reference/common/pom.xml +++ b/reference/common/pom.xml @@ -60,7 +60,7 @@ io.quarkus - quarkus-rest-client-jackson + quarkus-rest-client test diff --git a/reference/grpc/src/test/java/io/a2a/server/grpc/quarkus/A2ATestResource.java b/reference/grpc/src/test/java/io/a2a/server/grpc/quarkus/A2ATestResource.java index 6c24c5166..ef28ada75 100644 --- a/reference/grpc/src/test/java/io/a2a/server/grpc/quarkus/A2ATestResource.java +++ b/reference/grpc/src/test/java/io/a2a/server/grpc/quarkus/A2ATestResource.java @@ -24,7 +24,7 @@ import io.a2a.spec.TaskArtifactUpdateEvent; import io.a2a.spec.TaskStatusUpdateEvent; import io.a2a.transport.grpc.handler.GrpcHandler; -import io.a2a.util.Utils; +import io.a2a.json.JsonUtil; @Path("/test") @ApplicationScoped @@ -44,7 +44,7 @@ public void init() { @Path("/task") @Consumes(MediaType.APPLICATION_JSON) public Response saveTask(String body) throws Exception { - Task task = Utils.OBJECT_MAPPER.readValue(body, Task.class); + Task task = JsonUtil.fromJson(body, Task.class); testUtilsBean.saveTask(task); return Response.ok().build(); } @@ -57,7 +57,7 @@ public Response getTask(@PathParam("taskId") String taskId) throws Exception { return Response.status(404).build(); } return Response.ok() - .entity(Utils.OBJECT_MAPPER.writeValueAsString(task)) + .entity(JsonUtil.toJson(task)) .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) .build(); } @@ -85,7 +85,7 @@ public Response ensureQueue(@PathParam("taskId") String taskId) { @POST @Path("/queue/enqueueTaskStatusUpdateEvent/{taskId}") public Response enqueueTaskStatusUpdateEvent(@PathParam("taskId") String taskId, String body) throws Exception { - TaskStatusUpdateEvent event = Utils.OBJECT_MAPPER.readValue(body, TaskStatusUpdateEvent.class); + TaskStatusUpdateEvent event = JsonUtil.fromJson(body, TaskStatusUpdateEvent.class); testUtilsBean.enqueueEvent(taskId, event); return Response.ok().build(); } @@ -93,7 +93,7 @@ public Response enqueueTaskStatusUpdateEvent(@PathParam("taskId") String taskId, @POST @Path("/queue/enqueueTaskArtifactUpdateEvent/{taskId}") public Response enqueueTaskArtifactUpdateEvent(@PathParam("taskId") String taskId, String body) throws Exception { - TaskArtifactUpdateEvent event = Utils.OBJECT_MAPPER.readValue(body, TaskArtifactUpdateEvent.class); + TaskArtifactUpdateEvent event = JsonUtil.fromJson(body, TaskArtifactUpdateEvent.class); testUtilsBean.enqueueEvent(taskId, event); return Response.ok().build(); } @@ -130,7 +130,7 @@ public Response deleteTaskPushNotificationConfig(@PathParam("taskId") String tas @Path("/task/{taskId}") @Consumes(MediaType.APPLICATION_JSON) public Response savePushNotificationConfigInStore(@PathParam("taskId") String taskId, String body) throws Exception { - PushNotificationConfig notificationConfig = Utils.OBJECT_MAPPER.readValue(body, PushNotificationConfig.class); + PushNotificationConfig notificationConfig = JsonUtil.fromJson(body, PushNotificationConfig.class); if (notificationConfig == null) { return Response.status(404).build(); } diff --git a/reference/jsonrpc/pom.xml b/reference/jsonrpc/pom.xml index 22173625a..aa402bed1 100644 --- a/reference/jsonrpc/pom.xml +++ b/reference/jsonrpc/pom.xml @@ -64,7 +64,7 @@ io.quarkus - quarkus-rest-client-jackson + quarkus-rest-client test diff --git a/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java b/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java index df726ad88..7a4f8e76f 100644 --- a/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java +++ b/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java @@ -19,10 +19,9 @@ import jakarta.inject.Inject; import jakarta.inject.Singleton; -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.io.JsonEOFException; -import com.fasterxml.jackson.databind.JsonNode; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; +import io.a2a.json.JsonUtil; import io.a2a.common.A2AHeaders; import io.a2a.server.ServerCallContext; import io.a2a.server.auth.UnauthenticatedUser; @@ -35,19 +34,17 @@ import io.a2a.spec.GetAuthenticatedExtendedCardRequest; import io.a2a.spec.GetTaskPushNotificationConfigRequest; import io.a2a.spec.GetTaskRequest; -import io.a2a.spec.IdJsonMappingException; import io.a2a.spec.InternalError; import io.a2a.spec.InvalidParamsError; -import io.a2a.spec.InvalidParamsJsonMappingException; import io.a2a.spec.InvalidRequestError; import io.a2a.spec.JSONParseError; import io.a2a.spec.JSONRPCError; import io.a2a.spec.JSONRPCErrorResponse; +import io.a2a.spec.JSONRPCMessage; import io.a2a.spec.JSONRPCRequest; import io.a2a.spec.JSONRPCResponse; import io.a2a.spec.ListTaskPushNotificationConfigRequest; import io.a2a.spec.MethodNotFoundError; -import io.a2a.spec.MethodNotFoundJsonMappingException; import io.a2a.spec.NonStreamingJSONRPCRequest; import io.a2a.spec.SendMessageRequest; import io.a2a.spec.SendStreamingMessageRequest; @@ -94,26 +91,57 @@ public void invokeJSONRPCHandler(@Body String body, RoutingContext rc) { JSONRPCResponse nonStreamingResponse = null; Multi> streamingResponse = null; JSONRPCErrorResponse error = null; + Object requestId = null; try { - JsonNode node = Utils.OBJECT_MAPPER.readTree(body); - JsonNode method = node != null ? node.get("method") : null; - streaming = method != null && (SendStreamingMessageRequest.METHOD.equals(method.asText()) - || TaskResubscriptionRequest.METHOD.equals(method.asText())); - String methodName = (method != null && method.isTextual()) ? method.asText() : null; - if (methodName != null) { - context.getState().put(METHOD_NAME_KEY, methodName); + com.google.gson.JsonObject node; + try { + node = JsonParser.parseString(body).getAsJsonObject(); + } catch (Exception e) { + throw new JSONParseError(e.getMessage()); } + + // Validate jsonrpc field + com.google.gson.JsonElement jsonrpcElement = node.get("jsonrpc"); + if (jsonrpcElement == null || !jsonrpcElement.isJsonPrimitive() + || !JSONRPCMessage.JSONRPC_VERSION.equals(jsonrpcElement.getAsString())) { + throw new InvalidRequestError("Invalid JSON-RPC request: missing or invalid 'jsonrpc' field"); + } + + // Validate id field (must be string, number, or null — not an object or array) + com.google.gson.JsonElement idElement = node.get("id"); + if (idElement != null && !idElement.isJsonNull() && !idElement.isJsonPrimitive()) { + throw new InvalidRequestError("Invalid JSON-RPC request: 'id' must be a string, number, or null"); + } + if (idElement != null && !idElement.isJsonNull() && idElement.isJsonPrimitive()) { + com.google.gson.JsonPrimitive idPrimitive = idElement.getAsJsonPrimitive(); + requestId = idPrimitive.isNumber() ? idPrimitive.getAsLong() : idPrimitive.getAsString(); + } + + // Validate method field + com.google.gson.JsonElement methodElement = node.get("method"); + if (methodElement == null || !methodElement.isJsonPrimitive()) { + throw new InvalidRequestError("Invalid JSON-RPC request: missing or invalid 'method' field"); + } + + String methodName = methodElement.getAsString(); + context.getState().put(METHOD_NAME_KEY, methodName); + + streaming = SendStreamingMessageRequest.METHOD.equals(methodName) + || TaskResubscriptionRequest.METHOD.equals(methodName); + if (streaming) { - StreamingJSONRPCRequest request = Utils.OBJECT_MAPPER.treeToValue(node, StreamingJSONRPCRequest.class); + StreamingJSONRPCRequest request = deserializeStreamingRequest(body, methodName); streamingResponse = processStreamingRequest(request, context); } else { - NonStreamingJSONRPCRequest request = Utils.OBJECT_MAPPER.treeToValue(node, NonStreamingJSONRPCRequest.class); + NonStreamingJSONRPCRequest request = deserializeNonStreamingRequest(body, methodName); nonStreamingResponse = processNonStreamingRequest(request, context); } - } catch (JsonProcessingException e) { - error = handleError(e); + } catch (JSONRPCError e) { + error = new JSONRPCErrorResponse(requestId, e); + } catch (JsonSyntaxException e) { + error = new JSONRPCErrorResponse(requestId, new JSONParseError(e.getMessage())); } catch (Throwable t) { - error = new JSONRPCErrorResponse(new InternalError(t.getMessage())); + error = new JSONRPCErrorResponse(requestId, new InternalError(t.getMessage())); } finally { if (error != null) { rc.response() @@ -136,28 +164,6 @@ public void invokeJSONRPCHandler(@Body String body, RoutingContext rc) { } } - private JSONRPCErrorResponse handleError(JsonProcessingException exception) { - Object id = null; - JSONRPCError jsonRpcError = null; - if (exception.getCause() instanceof JsonParseException) { - jsonRpcError = new JSONParseError(); - } else if (exception instanceof JsonEOFException) { - jsonRpcError = new JSONParseError(exception.getMessage()); - } else if (exception instanceof MethodNotFoundJsonMappingException err) { - id = err.getId(); - jsonRpcError = new MethodNotFoundError(); - } else if (exception instanceof InvalidParamsJsonMappingException err) { - id = err.getId(); - jsonRpcError = new InvalidParamsError(); - } else if (exception instanceof IdJsonMappingException err) { - id = err.getId(); - jsonRpcError = new InvalidRequestError(); - } else { - jsonRpcError = new InvalidRequestError(); - } - return new JSONRPCErrorResponse(id, jsonRpcError); - } - /** * /** * Handles incoming GET requests to the agent card endpoint. @@ -170,6 +176,40 @@ public AgentCard getAgentCard() { return jsonRpcHandler.getAgentCard(); } + private NonStreamingJSONRPCRequest deserializeNonStreamingRequest(String body, String methodName) { + try { + return switch (methodName) { + case GetTaskRequest.METHOD -> JsonUtil.fromJson(body, GetTaskRequest.class); + case CancelTaskRequest.METHOD -> JsonUtil.fromJson(body, CancelTaskRequest.class); + case SendMessageRequest.METHOD -> JsonUtil.fromJson(body, SendMessageRequest.class); + case SetTaskPushNotificationConfigRequest.METHOD -> JsonUtil.fromJson(body, SetTaskPushNotificationConfigRequest.class); + case GetTaskPushNotificationConfigRequest.METHOD -> JsonUtil.fromJson(body, GetTaskPushNotificationConfigRequest.class); + case ListTaskPushNotificationConfigRequest.METHOD -> JsonUtil.fromJson(body, ListTaskPushNotificationConfigRequest.class); + case DeleteTaskPushNotificationConfigRequest.METHOD -> JsonUtil.fromJson(body, DeleteTaskPushNotificationConfigRequest.class); + case GetAuthenticatedExtendedCardRequest.METHOD -> JsonUtil.fromJson(body, GetAuthenticatedExtendedCardRequest.class); + default -> throw new MethodNotFoundError(); + }; + } catch (JSONRPCError e) { + throw e; + } catch (Exception e) { + throw new InvalidParamsError(e.getMessage()); + } + } + + private StreamingJSONRPCRequest deserializeStreamingRequest(String body, String methodName) { + try { + return switch (methodName) { + case SendStreamingMessageRequest.METHOD -> JsonUtil.fromJson(body, SendStreamingMessageRequest.class); + case TaskResubscriptionRequest.METHOD -> JsonUtil.fromJson(body, TaskResubscriptionRequest.class); + default -> throw new MethodNotFoundError(); + }; + } catch (JSONRPCError e) { + throw e; + } catch (Exception e) { + throw new InvalidParamsError(e.getMessage()); + } + } + private JSONRPCResponse processNonStreamingRequest( NonStreamingJSONRPCRequest request, ServerCallContext context) { if (request instanceof GetTaskRequest req) { diff --git a/reference/jsonrpc/src/test/java/io/a2a/server/apps/quarkus/A2ATestRoutes.java b/reference/jsonrpc/src/test/java/io/a2a/server/apps/quarkus/A2ATestRoutes.java index 3e6be1a31..d3bfca712 100644 --- a/reference/jsonrpc/src/test/java/io/a2a/server/apps/quarkus/A2ATestRoutes.java +++ b/reference/jsonrpc/src/test/java/io/a2a/server/apps/quarkus/A2ATestRoutes.java @@ -15,7 +15,7 @@ import io.a2a.spec.Task; import io.a2a.spec.TaskArtifactUpdateEvent; import io.a2a.spec.TaskStatusUpdateEvent; -import io.a2a.util.Utils; +import io.a2a.json.JsonUtil; import io.quarkus.vertx.web.Body; import io.quarkus.vertx.web.Param; import io.quarkus.vertx.web.Route; @@ -43,7 +43,7 @@ public void init() { @Route(path = "/test/task", methods = {Route.HttpMethod.POST}, consumes = {APPLICATION_JSON}, type = Route.HandlerType.BLOCKING) public void saveTask(@Body String body, RoutingContext rc) { try { - Task task = Utils.OBJECT_MAPPER.readValue(body, Task.class); + Task task = JsonUtil.fromJson(body, Task.class); testUtilsBean.saveTask(task); rc.response() .setStatusCode(200) @@ -66,7 +66,7 @@ public void getTask(@Param String taskId, RoutingContext rc) { rc.response() .setStatusCode(200) .putHeader(CONTENT_TYPE, APPLICATION_JSON) - .end(Utils.OBJECT_MAPPER.writeValueAsString(task)); + .end(JsonUtil.toJson(task)); } catch (Throwable t) { errorResponse(t, rc); @@ -108,7 +108,7 @@ public void ensureTaskQueue(@Param String taskId, RoutingContext rc) { public void enqueueTaskStatusUpdateEvent(@Param String taskId, @Body String body, RoutingContext rc) { try { - TaskStatusUpdateEvent event = Utils.OBJECT_MAPPER.readValue(body, TaskStatusUpdateEvent.class); + TaskStatusUpdateEvent event = JsonUtil.fromJson(body, TaskStatusUpdateEvent.class); testUtilsBean.enqueueEvent(taskId, event); rc.response() .setStatusCode(200) @@ -122,7 +122,7 @@ public void enqueueTaskStatusUpdateEvent(@Param String taskId, @Body String body public void enqueueTaskArtifactUpdateEvent(@Param String taskId, @Body String body, RoutingContext rc) { try { - TaskArtifactUpdateEvent event = Utils.OBJECT_MAPPER.readValue(body, TaskArtifactUpdateEvent.class); + TaskArtifactUpdateEvent event = JsonUtil.fromJson(body, TaskArtifactUpdateEvent.class); testUtilsBean.enqueueEvent(taskId, event); rc.response() .setStatusCode(200) @@ -169,7 +169,7 @@ public void deleteTaskPushNotificationConfig(@Param String taskId, @Param String @Route(path = "/test/task/:taskId", methods = {Route.HttpMethod.POST}, type = Route.HandlerType.BLOCKING) public void saveTaskPushNotificationConfig(@Param String taskId, @Body String body, RoutingContext rc) { try { - PushNotificationConfig notificationConfig = Utils.OBJECT_MAPPER.readValue(body, PushNotificationConfig.class); + PushNotificationConfig notificationConfig = JsonUtil.fromJson(body, PushNotificationConfig.class); if (notificationConfig == null) { rc.response() .setStatusCode(404) diff --git a/reference/rest/pom.xml b/reference/rest/pom.xml index 951704c29..ffec66fda 100644 --- a/reference/rest/pom.xml +++ b/reference/rest/pom.xml @@ -74,7 +74,7 @@ io.quarkus - quarkus-rest-client-jackson + quarkus-rest-client test diff --git a/reference/rest/src/test/java/io/a2a/server/rest/quarkus/A2ATestRoutes.java b/reference/rest/src/test/java/io/a2a/server/rest/quarkus/A2ATestRoutes.java index 6cda8dc8f..ba5bd1cfd 100644 --- a/reference/rest/src/test/java/io/a2a/server/rest/quarkus/A2ATestRoutes.java +++ b/reference/rest/src/test/java/io/a2a/server/rest/quarkus/A2ATestRoutes.java @@ -17,7 +17,7 @@ import io.a2a.spec.Task; import io.a2a.spec.TaskArtifactUpdateEvent; import io.a2a.spec.TaskStatusUpdateEvent; -import io.a2a.util.Utils; +import io.a2a.json.JsonUtil; import io.quarkus.vertx.web.Body; import io.quarkus.vertx.web.Param; import io.quarkus.vertx.web.Route; @@ -45,7 +45,7 @@ public void init() { @Route(path = "/test/task", methods = {Route.HttpMethod.POST}, consumes = {APPLICATION_JSON}, type = Route.HandlerType.BLOCKING) public void saveTask(@Body String body, RoutingContext rc) { try { - Task task = Utils.OBJECT_MAPPER.readValue(body, Task.class); + Task task = JsonUtil.fromJson(body, Task.class); testUtilsBean.saveTask(task); rc.response() .setStatusCode(200) @@ -68,7 +68,7 @@ public void getTask(@Param String taskId, RoutingContext rc) { rc.response() .setStatusCode(200) .putHeader(CONTENT_TYPE, APPLICATION_JSON) - .end(Utils.OBJECT_MAPPER.writeValueAsString(task)); + .end(JsonUtil.toJson(task)); } catch (Throwable t) { errorResponse(t, rc); @@ -110,7 +110,7 @@ public void ensureTaskQueue(@Param String taskId, RoutingContext rc) { public void enqueueTaskStatusUpdateEvent(@Param String taskId, @Body String body, RoutingContext rc) { try { - TaskStatusUpdateEvent event = Utils.OBJECT_MAPPER.readValue(body, TaskStatusUpdateEvent.class); + TaskStatusUpdateEvent event = JsonUtil.fromJson(body, TaskStatusUpdateEvent.class); testUtilsBean.enqueueEvent(taskId, event); rc.response() .setStatusCode(200) @@ -124,7 +124,7 @@ public void enqueueTaskStatusUpdateEvent(@Param String taskId, @Body String body public void enqueueTaskArtifactUpdateEvent(@Param String taskId, @Body String body, RoutingContext rc) { try { - TaskArtifactUpdateEvent event = Utils.OBJECT_MAPPER.readValue(body, TaskArtifactUpdateEvent.class); + TaskArtifactUpdateEvent event = JsonUtil.fromJson(body, TaskArtifactUpdateEvent.class); testUtilsBean.enqueueEvent(taskId, event); rc.response() .setStatusCode(200) @@ -171,7 +171,7 @@ public void deleteTaskPushNotificationConfig(@Param String taskId, @Param String @Route(path = "/test/task/:taskId", methods = {Route.HttpMethod.POST}, type = Route.HandlerType.BLOCKING) public void saveTaskPushNotificationConfig(@Param String taskId, @Body String body, RoutingContext rc) { try { - PushNotificationConfig notificationConfig = Utils.OBJECT_MAPPER.readValue(body, PushNotificationConfig.class); + PushNotificationConfig notificationConfig = JsonUtil.fromJson(body, PushNotificationConfig.class); if (notificationConfig == null) { rc.response() .setStatusCode(404) diff --git a/server-common/pom.xml b/server-common/pom.xml index ff2cd8a1c..a24035f52 100644 --- a/server-common/pom.xml +++ b/server-common/pom.xml @@ -29,14 +29,6 @@ ${project.groupId} a2a-java-sdk-client-transport-jsonrpc - - com.fasterxml.jackson.core - jackson-databind - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - io.smallrye.reactive mutiny-zero diff --git a/server-common/src/main/java/io/a2a/server/tasks/BasePushNotificationSender.java b/server-common/src/main/java/io/a2a/server/tasks/BasePushNotificationSender.java index 4afaf3b4e..9601e6b79 100644 --- a/server-common/src/main/java/io/a2a/server/tasks/BasePushNotificationSender.java +++ b/server-common/src/main/java/io/a2a/server/tasks/BasePushNotificationSender.java @@ -1,6 +1,7 @@ package io.a2a.server.tasks; import static io.a2a.common.A2AHeaders.X_A2A_NOTIFICATION_TOKEN; + import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; @@ -9,13 +10,13 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; -import com.fasterxml.jackson.core.JsonProcessingException; +import io.a2a.json.JsonProcessingException; import io.a2a.client.http.A2AHttpClient; import io.a2a.client.http.JdkA2AHttpClient; +import io.a2a.json.JsonUtil; import io.a2a.spec.PushNotificationConfig; import io.a2a.spec.Task; -import io.a2a.util.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -78,10 +79,7 @@ private boolean dispatchNotification(Task task, PushNotificationConfig pushInfo) String body; try { - body = Utils.OBJECT_MAPPER.writeValueAsString(task); - } catch (JsonProcessingException e) { - LOGGER.debug("Error writing value as string: {}", e.getMessage(), e); - return false; + body = JsonUtil.toJson(task); } catch (Throwable throwable) { LOGGER.debug("Error writing value as string: {}", throwable.getMessage(), throwable); return false; diff --git a/server-common/src/test/java/io/a2a/server/events/EventConsumerTest.java b/server-common/src/test/java/io/a2a/server/events/EventConsumerTest.java index 0cf633fff..3e5611d9e 100644 --- a/server-common/src/test/java/io/a2a/server/events/EventConsumerTest.java +++ b/server-common/src/test/java/io/a2a/server/events/EventConsumerTest.java @@ -14,7 +14,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -import com.fasterxml.jackson.core.JsonProcessingException; +import io.a2a.json.JsonProcessingException; import io.a2a.spec.A2AError; import io.a2a.spec.A2AServerException; import io.a2a.spec.Artifact; @@ -63,13 +63,13 @@ public void init() { @Test public void testConsumeOneTaskEvent() throws Exception { - Task event = Utils.unmarshalFrom(MINIMAL_TASK, Task.TYPE_REFERENCE); + Task event = Utils.unmarshalFrom(MINIMAL_TASK, Task.class); enqueueAndConsumeOneEvent(event); } @Test public void testConsumeOneMessageEvent() throws Exception { - Event event = Utils.unmarshalFrom(MESSAGE_PAYLOAD, Message.TYPE_REFERENCE); + Event event = Utils.unmarshalFrom(MESSAGE_PAYLOAD, Message.class); enqueueAndConsumeOneEvent(event); } @@ -93,7 +93,7 @@ public void testConsumeOneQueueEmpty() throws A2AServerException { @Test public void testConsumeAllMultipleEvents() throws JsonProcessingException { List events = List.of( - Utils.unmarshalFrom(MINIMAL_TASK, Task.TYPE_REFERENCE), + Utils.unmarshalFrom(MINIMAL_TASK, Task.class), new TaskArtifactUpdateEvent.Builder() .taskId("task-123") .contextId("session-xyz") @@ -154,7 +154,7 @@ public void onComplete() { @Test public void testConsumeUntilMessage() throws Exception { List events = List.of( - Utils.unmarshalFrom(MINIMAL_TASK, Task.TYPE_REFERENCE), + Utils.unmarshalFrom(MINIMAL_TASK, Task.class), new TaskArtifactUpdateEvent.Builder() .taskId("task-123") .contextId("session-xyz") @@ -214,7 +214,7 @@ public void onComplete() { @Test public void testConsumeMessageEvents() throws Exception { - Message message = Utils.unmarshalFrom(MESSAGE_PAYLOAD, Message.TYPE_REFERENCE); + Message message = Utils.unmarshalFrom(MESSAGE_PAYLOAD, Message.class); Message message2 = new Message.Builder(message).build(); List events = List.of(message, message2); @@ -393,7 +393,7 @@ public void testConsumeAllHandlesQueueClosedException() throws Exception { EventConsumer consumer = new EventConsumer(queue); // Add a message event (which will complete the stream) - Event message = Utils.unmarshalFrom(MESSAGE_PAYLOAD, Message.TYPE_REFERENCE); + Event message = Utils.unmarshalFrom(MESSAGE_PAYLOAD, Message.class); queue.enqueueEvent(message); // Close the queue before consuming diff --git a/server-common/src/test/java/io/a2a/server/events/EventQueueTest.java b/server-common/src/test/java/io/a2a/server/events/EventQueueTest.java index 389c21d88..75888f151 100644 --- a/server-common/src/test/java/io/a2a/server/events/EventQueueTest.java +++ b/server-common/src/test/java/io/a2a/server/events/EventQueueTest.java @@ -100,7 +100,7 @@ public void testEnqueueEventPropagagesToChildren() throws Exception { EventQueue parentQueue = EventQueue.builder().build(); EventQueue childQueue = parentQueue.tap(); - Event event = Utils.unmarshalFrom(MINIMAL_TASK, Task.TYPE_REFERENCE); + Event event = Utils.unmarshalFrom(MINIMAL_TASK, Task.class); parentQueue.enqueueEvent(event); // Event should be available in both parent and child queues @@ -117,8 +117,8 @@ public void testMultipleChildQueuesReceiveEvents() throws Exception { EventQueue childQueue1 = parentQueue.tap(); EventQueue childQueue2 = parentQueue.tap(); - Event event1 = Utils.unmarshalFrom(MINIMAL_TASK, Task.TYPE_REFERENCE); - Event event2 = Utils.unmarshalFrom(MESSAGE_PAYLOAD, Message.TYPE_REFERENCE); + Event event1 = Utils.unmarshalFrom(MINIMAL_TASK, Task.class); + Event event2 = Utils.unmarshalFrom(MESSAGE_PAYLOAD, Message.class); parentQueue.enqueueEvent(event1); parentQueue.enqueueEvent(event2); @@ -140,7 +140,7 @@ public void testChildQueueDequeueIndependently() throws Exception { EventQueue childQueue1 = parentQueue.tap(); EventQueue childQueue2 = parentQueue.tap(); - Event event = Utils.unmarshalFrom(MINIMAL_TASK, Task.TYPE_REFERENCE); + Event event = Utils.unmarshalFrom(MINIMAL_TASK, Task.class); parentQueue.enqueueEvent(event); // Dequeue from child1 first @@ -163,7 +163,7 @@ public void testCloseImmediatePropagationToChildren() throws Exception { EventQueue childQueue = parentQueue.tap(); // Add events to both parent and child - Event event = Utils.unmarshalFrom(MINIMAL_TASK, Task.TYPE_REFERENCE); + Event event = Utils.unmarshalFrom(MINIMAL_TASK, Task.class); parentQueue.enqueueEvent(event); assertFalse(childQueue.isClosed()); @@ -190,7 +190,7 @@ public void testCloseImmediatePropagationToChildren() throws Exception { @Test public void testEnqueueEventWhenClosed() throws Exception { EventQueue queue = EventQueue.builder().build(); - Event event = Utils.unmarshalFrom(MINIMAL_TASK, Task.TYPE_REFERENCE); + Event event = Utils.unmarshalFrom(MINIMAL_TASK, Task.class); queue.close(); // Close the queue first assertTrue(queue.isClosed()); @@ -220,7 +220,7 @@ public void testDequeueEventWhenClosedAndEmpty() throws Exception { @Test public void testDequeueEventWhenClosedButHasEvents() throws Exception { EventQueue queue = EventQueue.builder().build(); - Event event = Utils.unmarshalFrom(MINIMAL_TASK, Task.TYPE_REFERENCE); + Event event = Utils.unmarshalFrom(MINIMAL_TASK, Task.class); queue.enqueueEvent(event); queue.close(); // Graceful close - events should remain @@ -236,7 +236,7 @@ public void testDequeueEventWhenClosedButHasEvents() throws Exception { @Test public void testEnqueueAndDequeueEvent() throws Exception { - Event event = Utils.unmarshalFrom(MESSAGE_PAYLOAD, Message.TYPE_REFERENCE); + Event event = Utils.unmarshalFrom(MESSAGE_PAYLOAD, Message.class); eventQueue.enqueueEvent(event); Event dequeuedEvent = eventQueue.dequeueEventItem(200).getEvent(); assertSame(event, dequeuedEvent); @@ -244,7 +244,7 @@ public void testEnqueueAndDequeueEvent() throws Exception { @Test public void testDequeueEventNoWait() throws Exception { - Event event = Utils.unmarshalFrom(MINIMAL_TASK, Task.TYPE_REFERENCE); + Event event = Utils.unmarshalFrom(MINIMAL_TASK, Task.class); eventQueue.enqueueEvent(event); Event dequeuedEvent = eventQueue.dequeueEventItem(-1).getEvent(); assertSame(event, dequeuedEvent); @@ -305,7 +305,7 @@ public void testEnqueueDifferentEventTypes() throws Exception { */ @Test public void testCloseGracefulSetsFlag() throws Exception { - Event event = Utils.unmarshalFrom(MINIMAL_TASK, Task.TYPE_REFERENCE); + Event event = Utils.unmarshalFrom(MINIMAL_TASK, Task.class); eventQueue.enqueueEvent(event); eventQueue.close(false); // Graceful close @@ -318,7 +318,7 @@ public void testCloseGracefulSetsFlag() throws Exception { */ @Test public void testCloseImmediateClearsQueue() throws Exception { - Event event = Utils.unmarshalFrom(MINIMAL_TASK, Task.TYPE_REFERENCE); + Event event = Utils.unmarshalFrom(MINIMAL_TASK, Task.class); eventQueue.enqueueEvent(event); eventQueue.close(true); // Immediate close diff --git a/server-common/src/test/java/io/a2a/server/requesthandlers/AbstractA2ARequestHandlerTest.java b/server-common/src/test/java/io/a2a/server/requesthandlers/AbstractA2ARequestHandlerTest.java index d654a83a6..429c27942 100644 --- a/server-common/src/test/java/io/a2a/server/requesthandlers/AbstractA2ARequestHandlerTest.java +++ b/server-common/src/test/java/io/a2a/server/requesthandlers/AbstractA2ARequestHandlerTest.java @@ -37,7 +37,8 @@ import io.a2a.spec.TaskStatus; import io.a2a.spec.Event; import io.a2a.spec.TextPart; -import io.a2a.util.Utils; +import io.a2a.json.JsonProcessingException; +import io.a2a.json.JsonUtil; import io.quarkus.arc.profile.IfBuildProfile; import java.util.Map; @@ -199,8 +200,9 @@ public PostBuilder body(String body) { @Override public A2AHttpResponse post() throws IOException, InterruptedException { - tasks.add(Utils.OBJECT_MAPPER.readValue(body, Task.TYPE_REFERENCE)); try { + Task task = JsonUtil.fromJson(body, Task.class); + tasks.add(task); return new A2AHttpResponse() { @Override public int status() { @@ -217,8 +219,12 @@ public String body() { return ""; } }; + } catch (JsonProcessingException e) { + throw new IOException("Failed to parse task JSON", e); } finally { - latch.countDown(); + if (latch != null) { + latch.countDown(); + } } } diff --git a/server-common/src/test/java/io/a2a/server/tasks/InMemoryPushNotificationConfigStoreTest.java b/server-common/src/test/java/io/a2a/server/tasks/InMemoryPushNotificationConfigStoreTest.java index 9156f78b2..fc0ee6b1b 100644 --- a/server-common/src/test/java/io/a2a/server/tasks/InMemoryPushNotificationConfigStoreTest.java +++ b/server-common/src/test/java/io/a2a/server/tasks/InMemoryPushNotificationConfigStoreTest.java @@ -244,7 +244,7 @@ public void testSendNotificationSuccess() throws Exception { verify(mockPostBuilder).url(config.url()); verify(mockPostBuilder).body(bodyCaptor.capture()); verify(mockPostBuilder).post(); - + // Verify the request body contains the task data String sentBody = bodyCaptor.getValue(); assertTrue(sentBody.contains(task.getId())); diff --git a/server-common/src/test/java/io/a2a/server/tasks/InMemoryTaskStoreTest.java b/server-common/src/test/java/io/a2a/server/tasks/InMemoryTaskStoreTest.java index 1ae4174dc..d49f6de4f 100644 --- a/server-common/src/test/java/io/a2a/server/tasks/InMemoryTaskStoreTest.java +++ b/server-common/src/test/java/io/a2a/server/tasks/InMemoryTaskStoreTest.java @@ -19,7 +19,7 @@ public class InMemoryTaskStoreTest { @Test public void testSaveAndGet() throws Exception { InMemoryTaskStore store = new InMemoryTaskStore(); - Task task = Utils.unmarshalFrom(TASK_JSON, Task.TYPE_REFERENCE); + Task task = Utils.unmarshalFrom(TASK_JSON, Task.class); store.save(task); Task retrieved = store.get(task.getId()); assertSame(task, retrieved); @@ -35,7 +35,7 @@ public void testGetNonExistent() throws Exception { @Test public void testDelete() throws Exception { InMemoryTaskStore store = new InMemoryTaskStore(); - Task task = Utils.unmarshalFrom(TASK_JSON, Task.TYPE_REFERENCE); + Task task = Utils.unmarshalFrom(TASK_JSON, Task.class); store.save(task); store.delete(task.getId()); Task retrieved = store.get(task.getId()); diff --git a/server-common/src/test/java/io/a2a/server/tasks/PushNotificationSenderTest.java b/server-common/src/test/java/io/a2a/server/tasks/PushNotificationSenderTest.java index 2ab974edb..f73c2a7f5 100644 --- a/server-common/src/test/java/io/a2a/server/tasks/PushNotificationSenderTest.java +++ b/server-common/src/test/java/io/a2a/server/tasks/PushNotificationSenderTest.java @@ -19,7 +19,8 @@ import io.a2a.client.http.A2AHttpClient; import io.a2a.client.http.A2AHttpResponse; import io.a2a.common.A2AHeaders; -import io.a2a.util.Utils; +import io.a2a.json.JsonProcessingException; +import io.a2a.json.JsonUtil; import io.a2a.spec.PushNotificationConfig; import io.a2a.spec.Task; import io.a2a.spec.TaskState; @@ -72,13 +73,13 @@ public A2AHttpResponse post() throws IOException, InterruptedException { if (shouldThrowException) { throw new IOException("Simulated network error"); } - + try { - Task task = Utils.OBJECT_MAPPER.readValue(body, Task.TYPE_REFERENCE); + Task task = JsonUtil.fromJson(body, Task.class); tasks.add(task); urls.add(url); headers.add(new java.util.HashMap<>(requestHeaders)); - + return new A2AHttpResponse() { @Override public int status() { @@ -95,6 +96,8 @@ public String body() { return ""; } }; + } catch (JsonProcessingException e) { + throw new IOException("Failed to parse task JSON", e); } finally { if (latch != null) { latch.countDown(); diff --git a/server-common/src/test/java/io/a2a/server/tasks/TaskManagerTest.java b/server-common/src/test/java/io/a2a/server/tasks/TaskManagerTest.java index 1a4850bda..0b30b0a71 100644 --- a/server-common/src/test/java/io/a2a/server/tasks/TaskManagerTest.java +++ b/server-common/src/test/java/io/a2a/server/tasks/TaskManagerTest.java @@ -42,7 +42,7 @@ public class TaskManagerTest { @BeforeEach public void init() throws Exception { - minimalTask = Utils.unmarshalFrom(TASK_JSON, Task.TYPE_REFERENCE); + minimalTask = Utils.unmarshalFrom(TASK_JSON, Task.class); taskStore = new InMemoryTaskStore(); taskManager = new TaskManager(minimalTask.getId(), minimalTask.getContextId(), taskStore, null); } diff --git a/spec-grpc/pom.xml b/spec-grpc/pom.xml index 0ffac2fd3..a7efca27f 100644 --- a/spec-grpc/pom.xml +++ b/spec-grpc/pom.xml @@ -25,6 +25,10 @@ com.google.protobuf protobuf-java + + com.google.protobuf + protobuf-java-util + io.grpc grpc-protobuf diff --git a/spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java b/spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java new file mode 100644 index 000000000..5014608a7 --- /dev/null +++ b/spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java @@ -0,0 +1,546 @@ +package io.a2a.grpc.utils; + + +import io.a2a.json.JsonMappingException; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.Strictness; +import com.google.gson.stream.JsonWriter; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.JsonFormat; +import io.a2a.grpc.StreamResponse; +import io.a2a.spec.CancelTaskRequest; +import io.a2a.spec.CancelTaskResponse; +import io.a2a.spec.ContentTypeNotSupportedError; +import io.a2a.spec.DeleteTaskPushNotificationConfigRequest; +import io.a2a.spec.DeleteTaskPushNotificationConfigResponse; +import io.a2a.spec.GetAuthenticatedExtendedCardRequest; +import io.a2a.spec.GetAuthenticatedExtendedCardResponse; +import io.a2a.spec.GetTaskPushNotificationConfigRequest; +import io.a2a.spec.GetTaskPushNotificationConfigResponse; +import io.a2a.spec.GetTaskRequest; +import io.a2a.spec.GetTaskResponse; +import io.a2a.spec.IdJsonMappingException; +import io.a2a.spec.InvalidAgentResponseError; +import io.a2a.spec.InvalidParamsError; +import io.a2a.spec.InvalidParamsJsonMappingException; +import io.a2a.spec.InvalidRequestError; +import io.a2a.spec.JSONParseError; +import io.a2a.spec.JSONRPCError; +import io.a2a.spec.JSONRPCMessage; +import io.a2a.spec.JSONRPCRequest; +import io.a2a.spec.JSONRPCResponse; +import io.a2a.spec.ListTaskPushNotificationConfigRequest; +import io.a2a.spec.ListTaskPushNotificationConfigResponse; +import io.a2a.spec.MethodNotFoundError; +import io.a2a.spec.MethodNotFoundJsonMappingException; +import io.a2a.spec.PushNotificationNotSupportedError; +import io.a2a.spec.SendMessageRequest; +import io.a2a.spec.SendMessageResponse; +import io.a2a.spec.SendStreamingMessageRequest; +import io.a2a.spec.SetTaskPushNotificationConfigRequest; +import io.a2a.spec.SetTaskPushNotificationConfigResponse; +import io.a2a.spec.TaskResubscriptionRequest; +import io.a2a.json.JsonUtil; +import io.a2a.json.JsonProcessingException; +import io.a2a.spec.AgentCard; +import io.a2a.spec.TaskNotCancelableError; +import io.a2a.spec.TaskNotFoundError; +import io.a2a.spec.UnsupportedOperationError; +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.jspecify.annotations.Nullable; + +import static io.a2a.spec.A2AErrorCodes.CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.INTERNAL_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.INVALID_AGENT_RESPONSE_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.INVALID_PARAMS_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.INVALID_REQUEST_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.JSON_PARSE_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.METHOD_NOT_FOUND_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.PUSH_NOTIFICATION_NOT_SUPPORTED_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.TASK_NOT_CANCELABLE_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.TASK_NOT_FOUND_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.UNSUPPORTED_OPERATION_ERROR_CODE; + + +/** + * Utilities for converting between JSON-RPC 2.0 messages and Protocol Buffer objects. + *

+ * This class provides a unified strategy for handling JSON-RPC requests and responses in the A2A SDK + * by bridging the JSON-RPC transport layer with Protocol Buffer-based internal representations. + * + *

Conversion Strategy

+ * The conversion process follows a two-step approach: + *
    + *
  1. JSON → Proto: JSON-RPC messages are parsed using Gson, then converted to Protocol Buffer + * objects using Google's {@link JsonFormat} parser. This ensures consistent handling of field names, + * types, and nested structures according to the proto3 specification.
  2. + *
  3. Proto → Spec: Protocol Buffer objects are converted to A2A spec objects using + * {@link ProtoUtils.FromProto} converters, which handle type mappings and create immutable + * spec-compliant Java objects.
  4. + *
+ * + *

Request Processing Flow

+ *
+ * Incoming JSON-RPC Request
+ *   ↓ parseRequestBody(String)
+ * Validate version, id, method
+ *   ↓ parseMethodRequest()
+ * Parse params → Proto Builder
+ *   ↓ ProtoUtils.FromProto.*
+ * Create JSONRPCRequest<?> with spec objects
+ * 
+ * + *

Response Processing Flow

+ *
+ * Incoming JSON-RPC Response
+ *   ↓ parseResponseBody(String, String)
+ * Validate version, id, check for errors
+ *   ↓ Parse result/error
+ * Proto Builder → spec objects
+ *   ↓ ProtoUtils.FromProto.*
+ * Create JSONRPCResponse<?> with result or error
+ * 
+ * + *

Serialization Flow

+ *
+ * Proto MessageOrBuilder
+ *   ↓ JsonFormat.printer()
+ * Proto JSON string
+ *   ↓ Gson JsonWriter
+ * Complete JSON-RPC envelope
+ * 
+ * + *

Error Handling

+ * The class provides detailed error messages for common failure scenarios: + *
    + *
  • Missing/invalid method: Returns {@link MethodNotFoundError} with the invalid method name
  • + *
  • Invalid parameters: Returns {@link InvalidParamsError} with proto parsing details
  • + *
  • Protocol version mismatch: Returns {@link InvalidRequestError} with version info
  • + *
  • Missing/invalid id: Returns {@link InvalidRequestError} with id validation details
  • + *
+ * + *

Thread Safety

+ * This class is thread-safe. All methods are stateless and use immutable shared resources + * ({@link Gson} instance is thread-safe, proto builders are created per-invocation). + * + *

Usage Example

+ *
{@code
+ * // Parse incoming JSON-RPC request
+ * String jsonRequest = """
+ *     {"jsonrpc":"2.0","id":1,"method":"tasks.get","params":{"name":"tasks/task-123"}}
+ *     """;
+ * JSONRPCRequest request = JSONRPCUtils.parseRequestBody(jsonRequest);
+ *
+ * // Create JSON-RPC request from proto
+ * io.a2a.grpc.GetTaskRequest protoRequest = ...;
+ * String json = JSONRPCUtils.toJsonRPCRequest("req-1", "tasks.get", protoRequest);
+ *
+ * // Create JSON-RPC response from proto
+ * io.a2a.grpc.Task protoTask = ...;
+ * String response = JSONRPCUtils.toJsonRPCResultResponse("req-1", protoTask);
+ * }
+ * + * @see ProtoUtils + * @see JSONRPCRequest + * @see JSONRPCResponse + * @see JSON-RPC 2.0 Specification + */ +public class JSONRPCUtils { + + private static final Logger log = Logger.getLogger(JSONRPCUtils.class.getName()); + private static final Gson GSON = new GsonBuilder() + .setStrictness(Strictness.STRICT) + .create(); + + public static JSONRPCRequest parseRequestBody(String body) throws JsonMappingException { + JsonElement jelement = JsonParser.parseString(body); + JsonObject jsonRpc = jelement.getAsJsonObject(); + if (!jsonRpc.has("method")) { + throw new IdJsonMappingException( + "JSON-RPC request missing required 'method' field. Request must include: jsonrpc, id, method, and params.", + getIdIfPossible(jsonRpc)); + } + String version = getAndValidateJsonrpc(jsonRpc); + Object id = getAndValidateId(jsonRpc); + String method = jsonRpc.get("method").getAsString(); + JsonElement paramsNode = jsonRpc.get("params"); + + try { + return parseMethodRequest(version, id, method, paramsNode); + } catch (InvalidParamsError e) { + throw new InvalidParamsJsonMappingException(e.getMessage(), id); + } + } + + private static JSONRPCRequest parseMethodRequest(String version, Object id, String method, JsonElement paramsNode) throws InvalidParamsError, MethodNotFoundJsonMappingException { + switch (method) { + case GetTaskRequest.METHOD -> { + io.a2a.grpc.GetTaskRequest.Builder builder = io.a2a.grpc.GetTaskRequest.newBuilder(); + parseRequestBody(paramsNode, builder); + return new GetTaskRequest(version, id, method, ProtoUtils.FromProto.taskQueryParams(builder)); + } + case CancelTaskRequest.METHOD -> { + io.a2a.grpc.CancelTaskRequest.Builder builder = io.a2a.grpc.CancelTaskRequest.newBuilder(); + parseRequestBody(paramsNode, builder); + return new CancelTaskRequest(version, id, method, ProtoUtils.FromProto.taskIdParams(builder)); + } + case SetTaskPushNotificationConfigRequest.METHOD -> { + io.a2a.grpc.CreateTaskPushNotificationConfigRequest.Builder builder = io.a2a.grpc.CreateTaskPushNotificationConfigRequest.newBuilder(); + parseRequestBody(paramsNode, builder); + return new SetTaskPushNotificationConfigRequest(version, id, method, ProtoUtils.FromProto.taskPushNotificationConfig(builder)); + } + case GetTaskPushNotificationConfigRequest.METHOD -> { + io.a2a.grpc.GetTaskPushNotificationConfigRequest.Builder builder = io.a2a.grpc.GetTaskPushNotificationConfigRequest.newBuilder(); + parseRequestBody(paramsNode, builder); + return new GetTaskPushNotificationConfigRequest(version, id, method, ProtoUtils.FromProto.getTaskPushNotificationConfigParams(builder)); + } + case SendMessageRequest.METHOD -> { + io.a2a.grpc.SendMessageRequest.Builder builder = io.a2a.grpc.SendMessageRequest.newBuilder(); + parseRequestBody(paramsNode, builder); + return new SendMessageRequest(version, id, method, ProtoUtils.FromProto.messageSendParams(builder)); + } + case ListTaskPushNotificationConfigRequest.METHOD -> { + io.a2a.grpc.ListTaskPushNotificationConfigRequest.Builder builder = io.a2a.grpc.ListTaskPushNotificationConfigRequest.newBuilder(); + parseRequestBody(paramsNode, builder); + return new ListTaskPushNotificationConfigRequest(version, id, method, ProtoUtils.FromProto.listTaskPushNotificationConfigParams(builder)); + } + case DeleteTaskPushNotificationConfigRequest.METHOD -> { + io.a2a.grpc.DeleteTaskPushNotificationConfigRequest.Builder builder = io.a2a.grpc.DeleteTaskPushNotificationConfigRequest.newBuilder(); + parseRequestBody(paramsNode, builder); + return new DeleteTaskPushNotificationConfigRequest(version, id, method, ProtoUtils.FromProto.deleteTaskPushNotificationConfigParams(builder)); + } + case GetAuthenticatedExtendedCardRequest.METHOD -> { + return new GetAuthenticatedExtendedCardRequest(version, id, method, null); + } + case SendStreamingMessageRequest.METHOD -> { + io.a2a.grpc.SendMessageRequest.Builder builder = io.a2a.grpc.SendMessageRequest.newBuilder(); + parseRequestBody(paramsNode, builder); + return new SendStreamingMessageRequest(version, id, method, ProtoUtils.FromProto.messageSendParams(builder)); + } + case TaskResubscriptionRequest.METHOD -> { + io.a2a.grpc.TaskSubscriptionRequest.Builder builder = io.a2a.grpc.TaskSubscriptionRequest.newBuilder(); + parseRequestBody(paramsNode, builder); + return new TaskResubscriptionRequest(version, id, method, ProtoUtils.FromProto.taskIdParams(builder)); + } + default -> + throw new MethodNotFoundJsonMappingException("Unsupported JSON-RPC method: '" + method + "'", id); + } + } + + public static StreamResponse parseResponseEvent(String body) throws JsonMappingException { + JsonElement jelement = JsonParser.parseString(body); + JsonObject jsonRpc = jelement.getAsJsonObject(); + String version = getAndValidateJsonrpc(jsonRpc); + Object id = getAndValidateId(jsonRpc); + JsonElement paramsNode = jsonRpc.get("result"); + if (jsonRpc.has("error")) { + throw processError(jsonRpc.getAsJsonObject("error")); + } + StreamResponse.Builder builder = StreamResponse.newBuilder(); + parseRequestBody(paramsNode, builder); + return builder.build(); + } + + public static JSONRPCResponse parseResponseBody(String body, String method) throws JsonMappingException { + JsonElement jelement = JsonParser.parseString(body); + JsonObject jsonRpc = jelement.getAsJsonObject(); + String version = getAndValidateJsonrpc(jsonRpc); + Object id = getAndValidateId(jsonRpc); + JsonElement paramsNode = jsonRpc.get("result"); + if (jsonRpc.has("error")) { + return parseError(jsonRpc.getAsJsonObject("error"), id, method); + } + switch (method) { + case GetTaskRequest.METHOD -> { + io.a2a.grpc.Task.Builder builder = io.a2a.grpc.Task.newBuilder(); + parseRequestBody(paramsNode, builder); + return new GetTaskResponse(id, ProtoUtils.FromProto.task(builder)); + } + case CancelTaskRequest.METHOD -> { + io.a2a.grpc.Task.Builder builder = io.a2a.grpc.Task.newBuilder(); + parseRequestBody(paramsNode, builder); + return new CancelTaskResponse(id, ProtoUtils.FromProto.task(builder)); + } + case SetTaskPushNotificationConfigRequest.METHOD -> { + io.a2a.grpc.TaskPushNotificationConfig.Builder builder = io.a2a.grpc.TaskPushNotificationConfig.newBuilder(); + parseRequestBody(paramsNode, builder); + return new SetTaskPushNotificationConfigResponse(id, ProtoUtils.FromProto.taskPushNotificationConfig(builder)); + } + case GetTaskPushNotificationConfigRequest.METHOD -> { + io.a2a.grpc.TaskPushNotificationConfig.Builder builder = io.a2a.grpc.TaskPushNotificationConfig.newBuilder(); + parseRequestBody(paramsNode, builder); + return new GetTaskPushNotificationConfigResponse(id, ProtoUtils.FromProto.taskPushNotificationConfig(builder)); + } + case SendMessageRequest.METHOD -> { + io.a2a.grpc.SendMessageResponse.Builder builder = io.a2a.grpc.SendMessageResponse.newBuilder(); + parseRequestBody(paramsNode, builder); + if (builder.hasMsg()) { + return new SendMessageResponse(id, ProtoUtils.FromProto.message(builder.getMsg())); + } + return new SendMessageResponse(id, ProtoUtils.FromProto.task(builder.getTask())); + } + case ListTaskPushNotificationConfigRequest.METHOD -> { + io.a2a.grpc.ListTaskPushNotificationConfigResponse.Builder builder = io.a2a.grpc.ListTaskPushNotificationConfigResponse.newBuilder(); + parseRequestBody(paramsNode, builder); + return new ListTaskPushNotificationConfigResponse(id, ProtoUtils.FromProto.listTaskPushNotificationConfigParams(builder)); + } + case DeleteTaskPushNotificationConfigRequest.METHOD -> { + return new DeleteTaskPushNotificationConfigResponse(id); + } + case GetAuthenticatedExtendedCardRequest.METHOD -> { + try { + AgentCard card = JsonUtil.fromJson(GSON.toJson(paramsNode), AgentCard.class); + return new GetAuthenticatedExtendedCardResponse(id, card); + } catch (JsonProcessingException e) { + throw new InvalidParamsError("Failed to parse agent card response: " + e.getMessage()); + } + } + default -> + throw new MethodNotFoundJsonMappingException("Unsupported JSON-RPC method: '" + method + "' in response parsing.", getIdIfPossible(jsonRpc)); + } + } + + public static JSONRPCResponse parseError(JsonObject error, Object id, String method) throws JsonMappingException { + JSONRPCError rpcError = processError(error); + switch (method) { + case GetTaskRequest.METHOD -> { + return new GetTaskResponse(id, rpcError); + } + case CancelTaskRequest.METHOD -> { + return new CancelTaskResponse(id, rpcError); + } + case SetTaskPushNotificationConfigRequest.METHOD -> { + return new SetTaskPushNotificationConfigResponse(id, rpcError); + } + case GetTaskPushNotificationConfigRequest.METHOD -> { + return new GetTaskPushNotificationConfigResponse(id, rpcError); + } + case SendMessageRequest.METHOD -> { + return new SendMessageResponse(id, rpcError); + } + case ListTaskPushNotificationConfigRequest.METHOD -> { + return new ListTaskPushNotificationConfigResponse(id, rpcError); + } + case DeleteTaskPushNotificationConfigRequest.METHOD -> { + return new DeleteTaskPushNotificationConfigResponse(id, rpcError); + } + default -> + throw new MethodNotFoundJsonMappingException("Unsupported JSON-RPC method: '" + method + "'", id); + } + } + + private static JSONRPCError processError(JsonObject error) { + String message = error.has("message") ? error.get("message").getAsString() : null; + Integer code = error.has("code") ? error.get("code").getAsInt() : null; + String data = error.has("data") ? error.get("data").toString() : null; + if (code != null) { + switch (code) { + case JSON_PARSE_ERROR_CODE: + return new JSONParseError(code, message, data); + case INVALID_REQUEST_ERROR_CODE: + return new InvalidRequestError(code, message, data); + case METHOD_NOT_FOUND_ERROR_CODE: + return new MethodNotFoundError(code, message, data); + case INVALID_PARAMS_ERROR_CODE: + return new InvalidParamsError(code, message, data); + case INTERNAL_ERROR_CODE: + return new io.a2a.spec.InternalError(code, message, data); + case PUSH_NOTIFICATION_NOT_SUPPORTED_ERROR_CODE: + return new PushNotificationNotSupportedError(code, message, data); + case UNSUPPORTED_OPERATION_ERROR_CODE: + return new UnsupportedOperationError(code, message, data); + case CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE: + return new ContentTypeNotSupportedError(code, message, data); + case INVALID_AGENT_RESPONSE_ERROR_CODE: + return new InvalidAgentResponseError(code, message, data); + case TASK_NOT_CANCELABLE_ERROR_CODE: + return new TaskNotCancelableError(code, message, data); + case TASK_NOT_FOUND_ERROR_CODE: + return new TaskNotFoundError(code, message, data); + default: + return new JSONRPCError(code, message, data); + } + } + return new JSONRPCError(code, message, data); + } + + protected static void parseRequestBody(JsonElement jsonRpc, com.google.protobuf.Message.Builder builder) throws JSONRPCError { + try (Writer writer = new StringWriter()) { + GSON.toJson(jsonRpc, writer); + parseJsonString(writer.toString(), builder); + } catch (IOException e) { + log.log(Level.SEVERE, "Failed to serialize JSON element to string during proto conversion. JSON: {0}", jsonRpc); + log.log(Level.SEVERE, "Serialization error details", e); + throw new InvalidParamsError( + "Failed to parse request content. " + + "This may indicate invalid JSON structure or unsupported field types. Error: " + e.getMessage()); + } + } + + public static void parseJsonString(String body, com.google.protobuf.Message.Builder builder) throws JSONRPCError { + try { + JsonFormat.parser().merge(body, builder); + } catch (InvalidProtocolBufferException e) { + log.log(Level.SEVERE, "Protocol buffer parsing failed for JSON: {0}", body); + log.log(Level.SEVERE, "Proto parsing error details", e); + throw new InvalidParamsError( + "Invalid request content: " + extractProtoErrorMessage(e) + + ". Please verify the request matches the expected schema for this method."); + } + } + + /** + * Extracts a user-friendly error message from Protocol Buffer parsing exceptions. + * + * @param e the InvalidProtocolBufferException + * @return a cleaned error message with field information + */ + private static String extractProtoErrorMessage(InvalidProtocolBufferException e) { + String message = e.getMessage(); + if (message == null) { + return "unknown parsing error"; + } + // Extract field name if present in error message + if (message.contains("Cannot find field:")) { + return message.substring(message.indexOf("Cannot find field:")); + } + if (message.contains("Invalid value for")) { + return message.substring(message.indexOf("Invalid value for")); + } + return message; + } + + protected static String getAndValidateJsonrpc(JsonObject jsonRpc) throws JsonMappingException { + if (!jsonRpc.has("jsonrpc")) { + throw new IdJsonMappingException( + "Missing required 'jsonrpc' field. All requests must include 'jsonrpc': '2.0'", + getIdIfPossible(jsonRpc)); + } + String version = jsonRpc.get("jsonrpc").getAsString(); + if (!JSONRPCMessage.JSONRPC_VERSION.equals(version)) { + throw new IdJsonMappingException( + "Unsupported JSON-RPC version: '" + version + "'. Expected version '2.0'", + getIdIfPossible(jsonRpc)); + } + return version; + } + + /** + * Try to get the request id if possible , returns "UNDETERMINED ID" otherwise. + * This should be only used for errors. + * @param jsonRpc the json rpc JSON. + * @return the request id if possible , "UNDETERMINED ID" otherwise. + */ + protected static Object getIdIfPossible(JsonObject jsonRpc) { + try { + return getAndValidateId(jsonRpc); + } catch (JsonMappingException e) { + // id can't be determined + return "UNDETERMINED ID"; + } + } + + protected static Object getAndValidateId(JsonObject jsonRpc) throws JsonMappingException { + Object id = null; + if (jsonRpc.has("id")) { + if (jsonRpc.get("id").isJsonPrimitive()) { + try { + id = jsonRpc.get("id").getAsInt(); + } catch (UnsupportedOperationException | NumberFormatException | IllegalStateException e) { + id = jsonRpc.get("id").getAsString(); + } + } else { + throw new JsonMappingException(null, "Invalid 'id' type: " + jsonRpc.get("id").getClass().getSimpleName() + + ". ID must be a JSON string or number, not an object or array."); + } + } + if (id == null) { + throw new JsonMappingException(null, "Request 'id' cannot be null. Use a string or number identifier."); + } + return id; + } + + public static String toJsonRPCRequest(@Nullable String requestId, String method, com.google.protobuf.@Nullable MessageOrBuilder payload) { + try (StringWriter result = new StringWriter(); JsonWriter output = GSON.newJsonWriter(result)) { + output.beginObject(); + output.name("jsonrpc").value("2.0"); + String id = requestId; + if (requestId == null) { + id = UUID.randomUUID().toString(); + } + output.name("id").value(id); + if (method != null) { + output.name("method").value(method); + } + if (payload != null) { + String resultValue = JsonFormat.printer().omittingInsignificantWhitespace().print(payload); + output.name("params").jsonValue(resultValue); + } + output.endObject(); + return result.toString(); + } catch (IOException ex) { + throw new RuntimeException( + "Failed to serialize JSON-RPC request for method '" + method + "'. " + + "This indicates an internal error in JSON generation. Request ID: " + requestId, ex); + } + } + + public static String toJsonRPCResultResponse(Object requestId, com.google.protobuf.MessageOrBuilder builder) { + try (StringWriter result = new StringWriter(); JsonWriter output = GSON.newJsonWriter(result)) { + output.beginObject(); + output.name("jsonrpc").value("2.0"); + if (requestId != null) { + if (requestId instanceof String string) { + output.name("id").value(string); + } else if (requestId instanceof Number number) { + output.name("id").value(number.longValue()); + } + } + String resultValue = JsonFormat.printer().omittingInsignificantWhitespace().print(builder); + output.name("result").jsonValue(resultValue); + output.endObject(); + return result.toString(); + } catch (IOException ex) { + throw new RuntimeException( + "Failed to serialize JSON-RPC success response. " + + "Proto type: " + builder.getClass().getSimpleName() + ", Request ID: " + requestId, ex); + } + } + + public static String toJsonRPCErrorResponse(Object requestId, JSONRPCError error) { + try (StringWriter result = new StringWriter(); JsonWriter output = GSON.newJsonWriter(result)) { + output.beginObject(); + output.name("jsonrpc").value("2.0"); + if (requestId != null) { + if (requestId instanceof String string) { + output.name("id").value(string); + } else if (requestId instanceof Number number) { + output.name("id").value(number.longValue()); + } + } + output.name("error"); + output.beginObject(); + output.name("code").value(error.getCode()); + output.name("message").value(error.getMessage()); + if (error.getData() != null) { + output.name("data").value(error.getData().toString()); + } + output.endObject(); + output.endObject(); + return result.toString(); + } catch (IOException ex) { + throw new RuntimeException( + "Failed to serialize JSON-RPC error response. " + + "Error code: " + error.getCode() + ", Request ID: " + requestId, ex); + } + } +} diff --git a/spec-grpc/src/test/java/io/a2a/grpc/utils/JSONRPCUtilsTest.java b/spec-grpc/src/test/java/io/a2a/grpc/utils/JSONRPCUtilsTest.java new file mode 100644 index 000000000..0482f5ab3 --- /dev/null +++ b/spec-grpc/src/test/java/io/a2a/grpc/utils/JSONRPCUtilsTest.java @@ -0,0 +1,247 @@ +package io.a2a.grpc.utils; + +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 static org.junit.jupiter.api.Assertions.assertThrows; + +import io.a2a.json.JsonProcessingException; +import com.google.gson.JsonSyntaxException; +import io.a2a.spec.GetTaskPushNotificationConfigRequest; +import io.a2a.spec.GetTaskPushNotificationConfigResponse; +import io.a2a.spec.InvalidParamsError; +import io.a2a.spec.InvalidParamsJsonMappingException; +import io.a2a.spec.JSONParseError; +import io.a2a.spec.JSONRPCRequest; +import io.a2a.spec.SetTaskPushNotificationConfigRequest; +import io.a2a.spec.SetTaskPushNotificationConfigResponse; +import io.a2a.spec.TaskPushNotificationConfig; +import org.junit.jupiter.api.Test; + +public class JSONRPCUtilsTest { + + @Test + public void testParseSetTaskPushNotificationConfigRequest_ValidProtoFormat() throws JsonProcessingException { + String validRequest = """ + { + "jsonrpc": "2.0", + "method": "%s", + "id": "1", + "params": { + "parent": "tasks/task-123", + "configId": "config-456", + "config": { + "name": "tasks/task-123/pushNotificationConfigs/config-456", + "pushNotificationConfig": { + "url": "https://example.com/callback", + "authentication": { + "schemes": ["jwt"] + } + } + } + } + } + """.formatted(SetTaskPushNotificationConfigRequest.METHOD); + + JSONRPCRequest request = JSONRPCUtils.parseRequestBody(validRequest); + + assertNotNull(request); + assertInstanceOf(SetTaskPushNotificationConfigRequest.class, request); + SetTaskPushNotificationConfigRequest setRequest = (SetTaskPushNotificationConfigRequest) request; + assertEquals("2.0", setRequest.getJsonrpc()); + assertEquals(1, setRequest.getId()); + assertEquals(SetTaskPushNotificationConfigRequest.METHOD, setRequest.getMethod()); + + TaskPushNotificationConfig config = setRequest.getParams(); + assertNotNull(config); + assertEquals("task-123", config.taskId()); + assertNotNull(config.pushNotificationConfig()); + assertEquals("https://example.com/callback", config.pushNotificationConfig().url()); + } + + @Test + public void testParseGetTaskPushNotificationConfigRequest_ValidProtoFormat() throws JsonProcessingException { + String validRequest = """ + { + "jsonrpc": "2.0", + "method": "%s", + "id": "2", + "params": { + "name": "tasks/task-123/pushNotificationConfigs/config-456" + } + } + """.formatted(GetTaskPushNotificationConfigRequest.METHOD); + + JSONRPCRequest request = JSONRPCUtils.parseRequestBody(validRequest); + + assertNotNull(request); + assertInstanceOf(GetTaskPushNotificationConfigRequest.class, request); + GetTaskPushNotificationConfigRequest getRequest = (GetTaskPushNotificationConfigRequest) request; + assertEquals("2.0", getRequest.getJsonrpc()); + assertEquals(2, getRequest.getId()); + assertEquals(GetTaskPushNotificationConfigRequest.METHOD, getRequest.getMethod()); + assertNotNull(getRequest.getParams()); + assertEquals("task-123", getRequest.getParams().id()); + } + + @Test + public void testParseMalformedJSON_ThrowsJsonSyntaxException() { + String malformedRequest = """ + { + "jsonrpc": "2.0", + "method": "%s", + "params": { + "parent": "tasks/task-123" + """.formatted(SetTaskPushNotificationConfigRequest.METHOD); // Missing closing braces + + assertThrows(JsonSyntaxException.class, () -> { + JSONRPCUtils.parseRequestBody(malformedRequest); + }); + } + + @Test + public void testParseInvalidParams_ThrowsInvalidParamsError() { + String invalidParamsRequest = """ + { + "jsonrpc": "2.0", + "method": "%s", + "id": "3", + "params": "not_a_dict" + } + """.formatted(SetTaskPushNotificationConfigRequest.METHOD); + + InvalidParamsJsonMappingException exception = assertThrows( + InvalidParamsJsonMappingException.class, + () -> JSONRPCUtils.parseRequestBody(invalidParamsRequest) + ); + assertEquals(3, exception.getId()); + } + + @Test + public void testParseInvalidProtoStructure_ThrowsInvalidParamsError() { + String invalidStructure = """ + { + "jsonrpc": "2.0", + "method": "%s", + "id": "4", + "params": { + "invalid_field": "value" + } + } + """.formatted(SetTaskPushNotificationConfigRequest.METHOD); + + InvalidParamsJsonMappingException exception = assertThrows( + InvalidParamsJsonMappingException.class, + () -> JSONRPCUtils.parseRequestBody(invalidStructure) + ); + assertEquals(4, exception.getId()); + } + + @Test + public void testGenerateSetTaskPushNotificationConfigResponse_Success() throws Exception { + TaskPushNotificationConfig config = new TaskPushNotificationConfig( + "task-123", + new io.a2a.spec.PushNotificationConfig.Builder() + .url("https://example.com/callback") + .id("config-456") + .build() + ); + + String responseJson = """ + { + "jsonrpc": "2.0", + "id": "1", + "result": { + "name": "tasks/task-123/pushNotificationConfigs/config-456", + "pushNotificationConfig": { + "url": "https://example.com/callback", + "id": "config-456" + } + } + } + """; + + SetTaskPushNotificationConfigResponse response = + (SetTaskPushNotificationConfigResponse) JSONRPCUtils.parseResponseBody(responseJson, SetTaskPushNotificationConfigRequest.METHOD); + + assertNotNull(response); + assertEquals(1, response.getId()); + assertNotNull(response.getResult()); + assertEquals("task-123", response.getResult().taskId()); + assertEquals("https://example.com/callback", response.getResult().pushNotificationConfig().url()); + } + + @Test + public void testGenerateGetTaskPushNotificationConfigResponse_Success() throws Exception { + String responseJson = """ + { + "jsonrpc": "2.0", + "id": "2", + "result": { + "name": "tasks/task-123/pushNotificationConfigs/config-456", + "pushNotificationConfig": { + "url": "https://example.com/callback", + "id": "config-456" + } + } + } + """; + + GetTaskPushNotificationConfigResponse response = + (GetTaskPushNotificationConfigResponse) JSONRPCUtils.parseResponseBody(responseJson, GetTaskPushNotificationConfigRequest.METHOD); + + assertNotNull(response); + assertEquals(2, response.getId()); + assertNotNull(response.getResult()); + assertEquals("task-123", response.getResult().taskId()); + assertEquals("https://example.com/callback", response.getResult().pushNotificationConfig().url()); + } + + @Test + public void testParseErrorResponse_InvalidParams() throws Exception { + String errorResponse = """ + { + "jsonrpc": "2.0", + "id": "5", + "error": { + "code": -32602, + "message": "Invalid params" + } + } + """; + + SetTaskPushNotificationConfigResponse response = + (SetTaskPushNotificationConfigResponse) JSONRPCUtils.parseResponseBody(errorResponse, SetTaskPushNotificationConfigRequest.METHOD); + + assertNotNull(response); + assertEquals(5, response.getId()); + assertNotNull(response.getError()); + assertInstanceOf(InvalidParamsError.class, response.getError()); + assertEquals(-32602, response.getError().getCode()); + assertEquals("Invalid params", response.getError().getMessage()); + } + + @Test + public void testParseErrorResponse_ParseError() throws Exception { + String errorResponse = """ + { + "jsonrpc": "2.0", + "id": 6, + "error": { + "code": -32700, + "message": "Parse error" + } + } + """; + + SetTaskPushNotificationConfigResponse response = + (SetTaskPushNotificationConfigResponse) JSONRPCUtils.parseResponseBody(errorResponse, SetTaskPushNotificationConfigRequest.METHOD); + + assertNotNull(response); + assertEquals(6, response.getId()); + assertNotNull(response.getError()); + assertInstanceOf(JSONParseError.class, response.getError()); + assertEquals(-32700, response.getError().getCode()); + assertEquals("Parse error", response.getError().getMessage()); + } +} diff --git a/spec/pom.xml b/spec/pom.xml index ce100bf1e..f7f1f4bb0 100644 --- a/spec/pom.xml +++ b/spec/pom.xml @@ -21,14 +21,9 @@ ${project.groupId} a2a-java-sdk-common
- - - com.fasterxml.jackson.core - jackson-databind - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 + com.google.code.gson + gson diff --git a/spec/src/main/java/io/a2a/json/JsonMappingException.java b/spec/src/main/java/io/a2a/json/JsonMappingException.java new file mode 100644 index 000000000..550b7827c --- /dev/null +++ b/spec/src/main/java/io/a2a/json/JsonMappingException.java @@ -0,0 +1,102 @@ +package io.a2a.json; + +import org.jspecify.annotations.Nullable; + +/** + * Exception for JSON mapping errors when converting between JSON and Java objects. + *

+ * This exception serves as a replacement for Jackson's JsonMappingException, allowing + * the A2A Java SDK to remain independent of any specific JSON library implementation. + * It represents errors that occur during the mapping phase of deserialization or + * serialization, such as type mismatches, invalid values, or constraint violations. + *

+ * This exception extends {@link JsonProcessingException} and is used for more specific + * mapping-related errors compared to general parsing errors. + *

+ * Usage example: + *

{@code
+ * try {
+ *     Task task = JsonUtil.fromJson(json, Task.class);
+ *     if (task.getId() == null) {
+ *         throw new JsonMappingException(null, "Task ID cannot be null");
+ *     }
+ * } catch (JsonProcessingException e) {
+ *     throw new JsonMappingException(null, "Invalid task format: " + e.getMessage(), e);
+ * }
+ * }
+ * + * @see JsonProcessingException for the base exception class + */ +public class JsonMappingException extends JsonProcessingException { + + /** + * Optional reference object that caused the mapping error (e.g., JsonParser or field path). + */ + private final @Nullable Object reference; + + /** + * Constructs a new JsonMappingException with the specified reference and message. + *

+ * The reference parameter can be used to provide context about where the mapping + * error occurred (e.g., a field name, path, or parser reference). It can be null + * if no specific reference is available. + * + * @param reference optional reference object providing context for the error (may be null) + * @param message the detail message explaining the mapping error + */ + public JsonMappingException(@Nullable Object reference, String message) { + super(message); + this.reference = reference; + } + + /** + * Constructs a new JsonMappingException with the specified reference, message, and cause. + *

+ * The reference parameter can be used to provide context about where the mapping + * error occurred (e.g., a field name, path, or parser reference). It can be null + * if no specific reference is available. + * + * @param reference optional reference object providing context for the error (may be null) + * @param message the detail message explaining the mapping error + * @param cause the underlying cause of the mapping error (may be null) + */ + public JsonMappingException(@Nullable Object reference, String message, @Nullable Throwable cause) { + super(message, cause); + this.reference = reference; + } + + /** + * Constructs a new JsonMappingException with the specified message and cause. + *

+ * This constructor is provided for compatibility when no reference object is needed. + * + * @param message the detail message explaining the mapping error + * @param cause the underlying cause of the mapping error (may be null) + */ + public JsonMappingException(String message, @Nullable Throwable cause) { + this(null, message, cause); + } + + /** + * Constructs a new JsonMappingException with the specified message. + *

+ * This constructor is provided for compatibility when no reference object is needed. + * + * @param message the detail message explaining the mapping error + */ + public JsonMappingException(String message) { + this(null, message); + } + + /** + * Returns the reference object that provides context for the mapping error. + *

+ * This may be null if no specific reference was available when the exception + * was created. + * + * @return the reference object, or null if not available + */ + public @Nullable Object getReference() { + return reference; + } +} diff --git a/spec/src/main/java/io/a2a/json/JsonProcessingException.java b/spec/src/main/java/io/a2a/json/JsonProcessingException.java new file mode 100644 index 000000000..9af50b7ce --- /dev/null +++ b/spec/src/main/java/io/a2a/json/JsonProcessingException.java @@ -0,0 +1,55 @@ +package io.a2a.json; + +import org.jspecify.annotations.Nullable; + +/** + * General exception for JSON processing errors during serialization or deserialization. + *

+ * This exception serves as a replacement for Jackson's JsonProcessingException, allowing + * the A2A Java SDK to remain independent of any specific JSON library implementation. + * It can be used with any JSON processing library (Gson, Jackson, etc.). + *

+ * This is the base class for more specific JSON processing exceptions like + * {@link JsonMappingException}. + *

+ * Usage example: + *

{@code
+ * try {
+ *     String json = gson.toJson(object);
+ * } catch (Exception e) {
+ *     throw new JsonProcessingException("Failed to serialize object", e);
+ * }
+ * }
+ * + * @see JsonMappingException for mapping-specific errors + */ +public class JsonProcessingException extends Exception { + + /** + * Constructs a new JsonProcessingException with the specified message. + * + * @param message the detail message explaining the cause of the exception + */ + public JsonProcessingException(String message) { + super(message); + } + + /** + * Constructs a new JsonProcessingException with the specified message and cause. + * + * @param message the detail message explaining the cause of the exception + * @param cause the underlying cause of the exception (may be null) + */ + public JsonProcessingException(String message, @Nullable Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new JsonProcessingException with the specified cause. + * + * @param cause the underlying cause of the exception + */ + public JsonProcessingException(Throwable cause) { + super(cause); + } +} diff --git a/spec/src/main/java/io/a2a/json/JsonUtil.java b/spec/src/main/java/io/a2a/json/JsonUtil.java new file mode 100644 index 000000000..56dd3f310 --- /dev/null +++ b/spec/src/main/java/io/a2a/json/JsonUtil.java @@ -0,0 +1,904 @@ +package io.a2a.json; + +import static com.google.gson.stream.JsonToken.BEGIN_ARRAY; +import static com.google.gson.stream.JsonToken.BEGIN_OBJECT; +import static com.google.gson.stream.JsonToken.BOOLEAN; +import static com.google.gson.stream.JsonToken.NULL; +import static com.google.gson.stream.JsonToken.NUMBER; +import static com.google.gson.stream.JsonToken.STRING; +import static io.a2a.spec.A2AErrorCodes.CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.INTERNAL_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.INVALID_AGENT_RESPONSE_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.INVALID_PARAMS_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.INVALID_REQUEST_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.JSON_PARSE_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.METHOD_NOT_FOUND_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.PUSH_NOTIFICATION_NOT_SUPPORTED_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.TASK_NOT_CANCELABLE_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.TASK_NOT_FOUND_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.UNSUPPORTED_OPERATION_ERROR_CODE; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; +import com.google.gson.ToNumberPolicy; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import io.a2a.spec.APIKeySecurityScheme; +import io.a2a.spec.EventKind; +import io.a2a.spec.ContentTypeNotSupportedError; +import io.a2a.spec.DataPart; +import io.a2a.spec.FileContent; +import io.a2a.spec.FilePart; +import io.a2a.spec.FileWithBytes; +import io.a2a.spec.FileWithUri; +import io.a2a.spec.HTTPAuthSecurityScheme; +import io.a2a.spec.InvalidAgentResponseError; +import io.a2a.spec.InvalidParamsError; +import io.a2a.spec.InvalidRequestError; +import io.a2a.spec.JSONParseError; +import io.a2a.spec.JSONRPCError; +import io.a2a.spec.Message; +import io.a2a.spec.MethodNotFoundError; +import io.a2a.spec.MutualTLSSecurityScheme; +import io.a2a.spec.OAuth2SecurityScheme; +import io.a2a.spec.OpenIdConnectSecurityScheme; +import io.a2a.spec.Part; +import io.a2a.spec.PushNotificationNotSupportedError; +import io.a2a.spec.SecurityScheme; +import io.a2a.spec.StreamingEventKind; +import io.a2a.spec.Task; +import io.a2a.spec.TaskArtifactUpdateEvent; +import io.a2a.spec.TaskNotCancelableError; +import io.a2a.spec.TaskNotFoundError; +import io.a2a.spec.TaskState; +import io.a2a.spec.TaskStatusUpdateEvent; +import io.a2a.spec.TextPart; +import io.a2a.spec.UnsupportedOperationError; +import java.io.StringReader; +import java.lang.reflect.Type; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import org.jspecify.annotations.Nullable; + +import static io.a2a.json.JsonUtil.JSONRPCErrorTypeAdapter.THROWABLE_MARKER_FIELD; + +public class JsonUtil { + + private static GsonBuilder createBaseGsonBuilder() { + return new GsonBuilder() + .setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) + .registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeTypeAdapter()) + .registerTypeHierarchyAdapter(JSONRPCError.class, new JSONRPCErrorTypeAdapter()) + .registerTypeAdapter(TaskState.class, new TaskStateTypeAdapter()) + .registerTypeAdapter(Message.Role.class, new RoleTypeAdapter()) + .registerTypeAdapter(Part.Kind.class, new PartKindTypeAdapter()) + .registerTypeHierarchyAdapter(FileContent.class, new FileContentTypeAdapter()) + .registerTypeHierarchyAdapter(SecurityScheme.class, new SecuritySchemeTypeAdapter()); + } + + /** + * Pre-configured {@link Gson} instance for JSON operations. + *

+ * This mapper is configured with strict parsing mode and all necessary custom TypeAdapters + * for A2A Protocol types including polymorphic types, enums, and date/time types. + *

+ * Used throughout the SDK for consistent JSON serialization and deserialization. + * + * @see GsonFactory#createGson() + */ + public static final Gson OBJECT_MAPPER = createBaseGsonBuilder() + .registerTypeHierarchyAdapter(Part.class, new PartTypeAdapter()) + .registerTypeHierarchyAdapter(StreamingEventKind.class, new StreamingEventKindTypeAdapter()) + .registerTypeAdapter(EventKind.class, new EventKindTypeAdapter()) + .create(); + + public static T fromJson(String json, Class classOfT) throws JsonProcessingException { + try { + return OBJECT_MAPPER.fromJson(json, classOfT); + } catch (JsonSyntaxException e) { + throw new JsonProcessingException("Failed to parse JSON", e); + } + } + + public static T fromJson(String json, Type type) throws JsonProcessingException { + try { + return OBJECT_MAPPER.fromJson(json, type); + } catch (JsonSyntaxException e) { + throw new JsonProcessingException("Failed to parse JSON", e); + } + } + + /** + * Serializes an object to a JSON string using Gson. + *

+ * This method uses the pre-configured {@link #OBJECT_MAPPER} to produce + * JSON representation of the provided object. + * + * @param data the object to serialize + * @return JSON string representation of the object + */ + public static String toJson(Object data) throws JsonProcessingException { + try { + return OBJECT_MAPPER.toJson(data); + } catch (JsonSyntaxException e) { + throw new JsonProcessingException("Failed to generate JSON", e); + } + } + + /** + * Gson TypeAdapter for serializing and deserializing {@link OffsetDateTime} to/from ISO-8601 format. + *

+ * This adapter ensures that OffsetDateTime instances are serialized to ISO-8601 formatted strings + * (e.g., "2023-10-01T12:00:00.234-05:00") and deserialized from the same format. + * This is necessary because Gson cannot access private fields of java.time classes via reflection + * in Java 17+ due to module system restrictions. + *

+ * The adapter uses {@link DateTimeFormatter#ISO_OFFSET_DATE_TIME} for both serialization and + * deserialization, which ensures proper handling of timezone offsets. + * + * @see OffsetDateTime + * @see DateTimeFormatter#ISO_OFFSET_DATE_TIME + */ + static class OffsetDateTimeTypeAdapter extends TypeAdapter { + + @Override + public void write(JsonWriter out, OffsetDateTime value) throws java.io.IOException { + if (value == null) { + out.nullValue(); + } else { + out.value(value.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)); + } + } + + @Override + public @Nullable + OffsetDateTime read(JsonReader in) throws java.io.IOException { + if (in.peek() == com.google.gson.stream.JsonToken.NULL) { + in.nextNull(); + return null; + } + String dateTimeString = in.nextString(); + try { + return OffsetDateTime.parse(dateTimeString, DateTimeFormatter.ISO_OFFSET_DATE_TIME); + } catch (DateTimeParseException e) { + throw new JsonSyntaxException("Failed to parse OffsetDateTime: " + dateTimeString, e); + } + } + } + + /** + * Gson TypeAdapter for serializing and deserializing {@link Throwable} and its subclasses. + *

+ * This adapter avoids reflection into {@link Throwable}'s private fields, which is not allowed + * in Java 17+ due to module system restrictions. Instead, it serializes Throwables as simple + * objects containing only the type (fully qualified class name) and message. + *

+ * Serialization: Converts a Throwable to a JSON object with: + *

    + *
  • "type": The fully qualified class name (e.g., "java.lang.IllegalArgumentException")
  • + *
  • "message": The exception message
  • + *
+ *

+ * Deserialization: Reads the JSON and reconstructs the Throwable using reflection to find + * a constructor that accepts a String message parameter. If no such constructor exists or if + * instantiation fails, returns a generic {@link RuntimeException} with the message. + * + * @see Throwable + */ + static class ThrowableTypeAdapter extends TypeAdapter { + + @Override + public void write(JsonWriter out, Throwable value) throws java.io.IOException { + if (value == null) { + out.nullValue(); + return; + } + out.beginObject(); + out.name("type").value(value.getClass().getName()); + out.name("message").value(value.getMessage()); + out.name(THROWABLE_MARKER_FIELD).value(true); + out.endObject(); + } + + @Override + public @Nullable + Throwable read(JsonReader in) throws java.io.IOException { + if (in.peek() == com.google.gson.stream.JsonToken.NULL) { + in.nextNull(); + return null; + } + + String type = null; + String message = null; + + in.beginObject(); + while (in.hasNext()) { + String fieldName = in.nextName(); + switch (fieldName) { + case "type" -> + type = in.nextString(); + case "message" -> + message = in.nextString(); + default -> + in.skipValue(); + } + } + in.endObject(); + + // Try to reconstruct the Throwable + if (type != null) { + try { + Class throwableClass = Class.forName(type); + if (Throwable.class.isAssignableFrom(throwableClass)) { + // Try to find a constructor that takes a String message + try { + var constructor = throwableClass.getConstructor(String.class); + return (Throwable) constructor.newInstance(message); + } catch (NoSuchMethodException e) { + // No String constructor, return a generic RuntimeException + return new RuntimeException(message); + } + } + } catch (Exception e) { + // If we can't reconstruct the exact type, return a generic RuntimeException + return new RuntimeException(message); + } + } + return new RuntimeException(message); + } + } + + /** + * Gson TypeAdapter for serializing and deserializing {@link JSONRPCError} and its subclasses. + *

+ * This adapter handles polymorphic deserialization based on the error code, creating the + * appropriate subclass instance. + *

+ * The adapter maps error codes to their corresponding error classes: + *

    + *
  • -32700: {@link JSONParseError}
  • + *
  • -32600: {@link InvalidRequestError}
  • + *
  • -32601: {@link MethodNotFoundError}
  • + *
  • -32602: {@link InvalidParamsError}
  • + *
  • -32603: {@link InternalError}
  • + *
  • -32001: {@link TaskNotFoundError}
  • + *
  • -32002: {@link TaskNotCancelableError}
  • + *
  • -32003: {@link PushNotificationNotSupportedError}
  • + *
  • -32004: {@link UnsupportedOperationError}
  • + *
  • -32005: {@link ContentTypeNotSupportedError}
  • + *
  • -32006: {@link InvalidAgentResponseError}
  • + *
  • Other codes: {@link JSONRPCError}
  • + *
+ * + * @see JSONRPCError + */ + static class JSONRPCErrorTypeAdapter extends TypeAdapter { + + private static final ThrowableTypeAdapter THROWABLE_ADAPTER = new ThrowableTypeAdapter(); + static final String THROWABLE_MARKER_FIELD = "__throwable"; + private static final String CODE_FIELD = "code"; + private static final String DATA_FIELD = "data"; + private static final String MESSAGE_FIELD = "message"; + private static final String TYPE_FIELD = "type"; + + @Override + public void write(JsonWriter out, JSONRPCError value) throws java.io.IOException { + if (value == null) { + out.nullValue(); + return; + } + out.beginObject(); + out.name(CODE_FIELD).value(value.getCode()); + out.name(MESSAGE_FIELD).value(value.getMessage()); + if (value.getData() != null) { + out.name(DATA_FIELD); + // If data is a Throwable, use ThrowableTypeAdapter to avoid reflection issues + if (value.getData() instanceof Throwable throwable) { + THROWABLE_ADAPTER.write(out, throwable); + } else { + // Use Gson to serialize the data field for non-Throwable types + OBJECT_MAPPER.toJson(value.getData(), Object.class, out); + } + } + out.endObject(); + } + + @Override + public @Nullable + JSONRPCError read(JsonReader in) throws java.io.IOException { + if (in.peek() == com.google.gson.stream.JsonToken.NULL) { + in.nextNull(); + return null; + } + + Integer code = null; + String message = null; + Object data = null; + + in.beginObject(); + while (in.hasNext()) { + String fieldName = in.nextName(); + switch (fieldName) { + case CODE_FIELD -> + code = in.nextInt(); + case MESSAGE_FIELD -> + message = in.nextString(); + case DATA_FIELD -> { + // Read data as a generic object (could be string, number, object, etc.) + data = readDataValue(in); + } + default -> + in.skipValue(); + } + } + in.endObject(); + + // Create the appropriate subclass based on the error code + return createErrorInstance(code, message, data); + } + + /** + * Reads the data field value, which can be of any JSON type. + */ + private @Nullable + Object readDataValue(JsonReader in) throws java.io.IOException { + return switch (in.peek()) { + case STRING -> + in.nextString(); + case NUMBER -> + in.nextDouble(); + case BOOLEAN -> + in.nextBoolean(); + case NULL -> { + in.nextNull(); + yield null; + } + case BEGIN_OBJECT -> { + // Parse as JsonElement to check if it's a Throwable + com.google.gson.JsonElement element = com.google.gson.JsonParser.parseReader(in); + if (element.isJsonObject()) { + com.google.gson.JsonObject obj = element.getAsJsonObject(); + // Check if it has the structure of a serialized Throwable (type + message) + if (obj.has(TYPE_FIELD) && obj.has(MESSAGE_FIELD) && obj.has(THROWABLE_MARKER_FIELD)) { + // Deserialize as Throwable using ThrowableTypeAdapter + yield THROWABLE_ADAPTER.read(new JsonReader(new StringReader(element.toString()))); + } + } + // Otherwise, deserialize as generic object + yield OBJECT_MAPPER.fromJson(element, Object.class); + } + case BEGIN_ARRAY -> + // For arrays, read as raw JSON using Gson + OBJECT_MAPPER.fromJson(in, Object.class); + default -> { + in.skipValue(); + yield null; + } + }; + } + + /** + * Creates the appropriate JSONRPCError subclass based on the error code. + */ + private JSONRPCError createErrorInstance(@Nullable Integer code, @Nullable String message, @Nullable Object data) { + if (code == null) { + throw new JsonSyntaxException("JSONRPCError must have a code field"); + } + + return switch (code) { + case JSON_PARSE_ERROR_CODE -> + new JSONParseError(code, message, data); + case INVALID_REQUEST_ERROR_CODE -> + new InvalidRequestError(code, message, data); + case METHOD_NOT_FOUND_ERROR_CODE -> + new MethodNotFoundError(code, message, data); + case INVALID_PARAMS_ERROR_CODE -> + new InvalidParamsError(code, message, data); + case INTERNAL_ERROR_CODE -> + new io.a2a.spec.InternalError(code, message, data); + case TASK_NOT_FOUND_ERROR_CODE -> + new TaskNotFoundError(code, message, data); + case TASK_NOT_CANCELABLE_ERROR_CODE -> + new TaskNotCancelableError(code, message, data); + case PUSH_NOTIFICATION_NOT_SUPPORTED_ERROR_CODE -> + new PushNotificationNotSupportedError(code, message, data); + case UNSUPPORTED_OPERATION_ERROR_CODE -> + new UnsupportedOperationError(code, message, data); + case CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE -> + new ContentTypeNotSupportedError(code, message, data); + case INVALID_AGENT_RESPONSE_ERROR_CODE -> + new InvalidAgentResponseError(code, message, data); + default -> + new JSONRPCError(code, message, data); + }; + } + } + + /** + * Gson TypeAdapter for serializing and deserializing {@link TaskState} enum. + *

+ * This adapter ensures that TaskState enum values are serialized using their + * wire format string representation (e.g., "completed", "working") rather than + * the Java enum constant name (e.g., "COMPLETED", "WORKING"). + *

+ * For serialization, it uses {@link TaskState#asString()} to get the wire format. + * For deserialization, it uses {@link TaskState#fromString(String)} to parse the + * wire format back to the enum constant. + * + * @see TaskState + * @see TaskState#asString() + * @see TaskState#fromString(String) + */ + static class TaskStateTypeAdapter extends TypeAdapter { + + @Override + public void write(JsonWriter out, TaskState value) throws java.io.IOException { + if (value == null) { + out.nullValue(); + } else { + out.value(value.asString()); + } + } + + @Override + public @Nullable + TaskState read(JsonReader in) throws java.io.IOException { + if (in.peek() == com.google.gson.stream.JsonToken.NULL) { + in.nextNull(); + return null; + } + String stateString = in.nextString(); + try { + return TaskState.fromString(stateString); + } catch (IllegalArgumentException e) { + throw new JsonSyntaxException("Invalid TaskState: " + stateString, e); + } + } + } + + /** + * Gson TypeAdapter for serializing and deserializing {@link Message.Role} enum. + *

+ * This adapter ensures that Message.Role enum values are serialized using their + * wire format string representation (e.g., "user", "agent") rather than the Java + * enum constant name (e.g., "USER", "AGENT"). + *

+ * For serialization, it uses {@link Message.Role#asString()} to get the wire format. + * For deserialization, it parses the string to the enum constant. + * + * @see Message.Role + * @see Message.Role#asString() + */ + static class RoleTypeAdapter extends TypeAdapter { + + @Override + public void write(JsonWriter out, Message.Role value) throws java.io.IOException { + if (value == null) { + out.nullValue(); + } else { + out.value(value.asString()); + } + } + + @Override + public Message.@Nullable Role read(JsonReader in) throws java.io.IOException { + if (in.peek() == com.google.gson.stream.JsonToken.NULL) { + in.nextNull(); + return null; + } + String roleString = in.nextString(); + try { + return switch (roleString) { + case "user" -> + Message.Role.USER; + case "agent" -> + Message.Role.AGENT; + default -> + throw new IllegalArgumentException("Invalid Role: " + roleString); + }; + } catch (IllegalArgumentException e) { + throw new JsonSyntaxException("Invalid Message.Role: " + roleString, e); + } + } + } + + /** + * Gson TypeAdapter for serializing and deserializing {@link Part.Kind} enum. + *

+ * This adapter ensures that Part.Kind enum values are serialized using their + * wire format string representation (e.g., "text", "file", "data") rather than + * the Java enum constant name (e.g., "TEXT", "FILE", "DATA"). + *

+ * For serialization, it uses {@link Part.Kind#asString()} to get the wire format. + * For deserialization, it parses the string to the enum constant. + * + * @see Part.Kind + * @see Part.Kind#asString() + */ + static class PartKindTypeAdapter extends TypeAdapter { + + @Override + public void write(JsonWriter out, Part.Kind value) throws java.io.IOException { + if (value == null) { + out.nullValue(); + } else { + out.value(value.asString()); + } + } + + @Override + public Part.@Nullable Kind read(JsonReader in) throws java.io.IOException { + if (in.peek() == com.google.gson.stream.JsonToken.NULL) { + in.nextNull(); + return null; + } + String kindString = in.nextString(); + try { + return switch (kindString) { + case "text" -> + Part.Kind.TEXT; + case "file" -> + Part.Kind.FILE; + case "data" -> + Part.Kind.DATA; + default -> + throw new IllegalArgumentException("Invalid Part.Kind: " + kindString); + }; + } catch (IllegalArgumentException e) { + throw new JsonSyntaxException("Invalid Part.Kind: " + kindString, e); + } + } + } + + /** + * Gson TypeAdapter for serializing and deserializing {@link Part} and its subclasses. + *

+ * This adapter handles polymorphic deserialization based on the "kind" field, creating the + * appropriate subclass instance (TextPart, FilePart, or DataPart). + *

+ * The adapter uses a two-pass approach: first reads the JSON as a tree to inspect the "kind" + * field, then deserializes to the appropriate concrete type. + * + * @see Part + * @see TextPart + * @see FilePart + * @see DataPart + */ + static class PartTypeAdapter extends TypeAdapter> { + + // Create separate Gson instance without the Part adapter to avoid recursion + private final Gson delegateGson = createBaseGsonBuilder().create(); + + @Override + public void write(JsonWriter out, Part value) throws java.io.IOException { + if (value == null) { + out.nullValue(); + return; + } + // Delegate to Gson's default serialization for the concrete type + if (value instanceof TextPart textPart) { + delegateGson.toJson(textPart, TextPart.class, out); + } else if (value instanceof FilePart filePart) { + delegateGson.toJson(filePart, FilePart.class, out); + } else if (value instanceof DataPart dataPart) { + delegateGson.toJson(dataPart, DataPart.class, out); + } else { + throw new JsonSyntaxException("Unknown Part subclass: " + value.getClass().getName()); + } + } + + @Override + public @Nullable + Part read(JsonReader in) throws java.io.IOException { + if (in.peek() == com.google.gson.stream.JsonToken.NULL) { + in.nextNull(); + return null; + } + + // Read the JSON as a tree so we can inspect the "kind" field + com.google.gson.JsonElement jsonElement = com.google.gson.JsonParser.parseReader(in); + if (!jsonElement.isJsonObject()) { + throw new JsonSyntaxException("Part must be a JSON object"); + } + + com.google.gson.JsonObject jsonObject = jsonElement.getAsJsonObject(); + com.google.gson.JsonElement kindElement = jsonObject.get("kind"); + if (kindElement == null || !kindElement.isJsonPrimitive()) { + throw new JsonSyntaxException("Part must have a 'kind' field"); + } + + String kind = kindElement.getAsString(); + // Use the delegate Gson to deserialize to the concrete type + return switch (kind) { + case "text" -> + delegateGson.fromJson(jsonElement, TextPart.class); + case "file" -> + delegateGson.fromJson(jsonElement, FilePart.class); + case "data" -> + delegateGson.fromJson(jsonElement, DataPart.class); + default -> + throw new JsonSyntaxException("Unknown Part kind: " + kind); + }; + } + } + + /** + * Gson TypeAdapter for serializing and deserializing {@link EventKind} and its implementations. + *

+ * Discriminates based on the {@code "kind"} field: + *

    + *
  • {@code "task"} → {@link Task}
  • + *
  • {@code "message"} → {@link Message}
  • + *
+ */ + static class EventKindTypeAdapter extends TypeAdapter { + + private final Gson delegateGson = createBaseGsonBuilder() + .registerTypeHierarchyAdapter(Part.class, new PartTypeAdapter()) + .create(); + + @Override + public void write(JsonWriter out, EventKind value) throws java.io.IOException { + if (value == null) { + out.nullValue(); + return; + } + if (value instanceof Task task) { + delegateGson.toJson(task, Task.class, out); + } else if (value instanceof Message message) { + delegateGson.toJson(message, Message.class, out); + } else { + throw new JsonSyntaxException("Unknown EventKind implementation: " + value.getClass().getName()); + } + } + + @Override + public @Nullable EventKind read(JsonReader in) throws java.io.IOException { + if (in.peek() == com.google.gson.stream.JsonToken.NULL) { + in.nextNull(); + return null; + } + + com.google.gson.JsonElement jsonElement = com.google.gson.JsonParser.parseReader(in); + if (!jsonElement.isJsonObject()) { + throw new JsonSyntaxException("EventKind must be a JSON object"); + } + + com.google.gson.JsonObject jsonObject = jsonElement.getAsJsonObject(); + com.google.gson.JsonElement kindElement = jsonObject.get("kind"); + if (kindElement == null || !kindElement.isJsonPrimitive()) { + throw new JsonSyntaxException("EventKind must have a 'kind' field"); + } + + String kind = kindElement.getAsString(); + return switch (kind) { + case Task.TASK -> delegateGson.fromJson(jsonElement, Task.class); + case Message.MESSAGE -> delegateGson.fromJson(jsonElement, Message.class); + default -> throw new JsonSyntaxException("Unknown EventKind kind: " + kind); + }; + } + } + + /** + * Gson TypeAdapter for serializing and deserializing {@link StreamingEventKind} and its implementations. + *

+ * This adapter handles polymorphic deserialization based on the "kind" field, creating the + * appropriate implementation instance (Task, Message, TaskStatusUpdateEvent, or TaskArtifactUpdateEvent). + *

+ * The adapter uses a two-pass approach: first reads the JSON as a tree to inspect the "kind" + * field, then deserializes to the appropriate concrete type. + * + * @see StreamingEventKind + * @see Task + * @see Message + * @see TaskStatusUpdateEvent + * @see TaskArtifactUpdateEvent + */ + static class StreamingEventKindTypeAdapter extends TypeAdapter { + + // Create separate Gson instance without the StreamingEventKind adapter to avoid recursion + private final Gson delegateGson = createBaseGsonBuilder() + .registerTypeHierarchyAdapter(Part.class, new PartTypeAdapter()) + .create(); + + @Override + public void write(JsonWriter out, StreamingEventKind value) throws java.io.IOException { + if (value == null) { + out.nullValue(); + return; + } + // Delegate to Gson's default serialization for the concrete type + if (value instanceof Task task) { + delegateGson.toJson(task, Task.class, out); + } else if (value instanceof Message message) { + delegateGson.toJson(message, Message.class, out); + } else if (value instanceof TaskStatusUpdateEvent event) { + delegateGson.toJson(event, TaskStatusUpdateEvent.class, out); + } else if (value instanceof TaskArtifactUpdateEvent event) { + delegateGson.toJson(event, TaskArtifactUpdateEvent.class, out); + } else { + throw new JsonSyntaxException("Unknown StreamingEventKind implementation: " + value.getClass().getName()); + } + } + + @Override + public @Nullable + StreamingEventKind read(JsonReader in) throws java.io.IOException { + if (in.peek() == com.google.gson.stream.JsonToken.NULL) { + in.nextNull(); + return null; + } + + // Read the JSON as a tree so we can inspect the "kind" field + com.google.gson.JsonElement jsonElement = com.google.gson.JsonParser.parseReader(in); + if (!jsonElement.isJsonObject()) { + throw new JsonSyntaxException("StreamingEventKind must be a JSON object"); + } + + com.google.gson.JsonObject jsonObject = jsonElement.getAsJsonObject(); + com.google.gson.JsonElement kindElement = jsonObject.get("kind"); + if (kindElement == null || !kindElement.isJsonPrimitive()) { + throw new JsonSyntaxException("StreamingEventKind must have a 'kind' field"); + } + + String kind = kindElement.getAsString(); + // Use the delegate Gson to deserialize to the concrete type + return switch (kind) { + case "task" -> + delegateGson.fromJson(jsonElement, Task.class); + case "message" -> + delegateGson.fromJson(jsonElement, Message.class); + case "status-update" -> + delegateGson.fromJson(jsonElement, TaskStatusUpdateEvent.class); + case "artifact-update" -> + delegateGson.fromJson(jsonElement, TaskArtifactUpdateEvent.class); + default -> + throw new JsonSyntaxException("Unknown StreamingEventKind kind: " + kind); + }; + } + } + + /** + * Gson TypeAdapter for serializing and deserializing {@link SecurityScheme} and its implementations. + *

+ * Discriminates based on the {@code "type"} field: + *

    + *
  • {@code "apiKey"} → {@link APIKeySecurityScheme}
  • + *
  • {@code "http"} → {@link HTTPAuthSecurityScheme}
  • + *
  • {@code "oauth2"} → {@link OAuth2SecurityScheme}
  • + *
  • {@code "openIdConnect"} → {@link OpenIdConnectSecurityScheme}
  • + *
  • {@code "mutualTLS"} → {@link MutualTLSSecurityScheme}
  • + *
+ */ + static class SecuritySchemeTypeAdapter extends TypeAdapter { + + // Use a plain Gson to avoid circular initialization — SecurityScheme concrete types + // contain only simple fields (Strings, OAuthFlows) that need no custom adapters. + private final Gson delegateGson = new Gson(); + + @Override + public void write(JsonWriter out, SecurityScheme value) throws java.io.IOException { + if (value == null) { + out.nullValue(); + return; + } + if (value instanceof APIKeySecurityScheme v) { + delegateGson.toJson(v, APIKeySecurityScheme.class, out); + } else if (value instanceof HTTPAuthSecurityScheme v) { + delegateGson.toJson(v, HTTPAuthSecurityScheme.class, out); + } else if (value instanceof OAuth2SecurityScheme v) { + delegateGson.toJson(v, OAuth2SecurityScheme.class, out); + } else if (value instanceof OpenIdConnectSecurityScheme v) { + delegateGson.toJson(v, OpenIdConnectSecurityScheme.class, out); + } else if (value instanceof MutualTLSSecurityScheme v) { + delegateGson.toJson(v, MutualTLSSecurityScheme.class, out); + } else { + throw new JsonSyntaxException("Unknown SecurityScheme implementation: " + value.getClass().getName()); + } + } + + @Override + public @Nullable SecurityScheme read(JsonReader in) throws java.io.IOException { + if (in.peek() == com.google.gson.stream.JsonToken.NULL) { + in.nextNull(); + return null; + } + + com.google.gson.JsonElement jsonElement = com.google.gson.JsonParser.parseReader(in); + if (!jsonElement.isJsonObject()) { + throw new JsonSyntaxException("SecurityScheme must be a JSON object"); + } + + com.google.gson.JsonObject jsonObject = jsonElement.getAsJsonObject(); + com.google.gson.JsonElement typeElement = jsonObject.get("type"); + if (typeElement == null || !typeElement.isJsonPrimitive()) { + throw new JsonSyntaxException("SecurityScheme must have a 'type' field"); + } + + String type = typeElement.getAsString(); + return switch (type) { + case APIKeySecurityScheme.API_KEY -> + delegateGson.fromJson(jsonElement, APIKeySecurityScheme.class); + case HTTPAuthSecurityScheme.HTTP -> + delegateGson.fromJson(jsonElement, HTTPAuthSecurityScheme.class); + case OAuth2SecurityScheme.OAUTH2 -> + delegateGson.fromJson(jsonElement, OAuth2SecurityScheme.class); + case OpenIdConnectSecurityScheme.OPENID_CONNECT -> + delegateGson.fromJson(jsonElement, OpenIdConnectSecurityScheme.class); + case MutualTLSSecurityScheme.MUTUAL_TLS -> + delegateGson.fromJson(jsonElement, MutualTLSSecurityScheme.class); + default -> + throw new JsonSyntaxException("Unknown SecurityScheme type: " + type); + }; + } + } + + /** + * Gson TypeAdapter for serializing and deserializing {@link FileContent} and its implementations. + *

+ * This adapter handles polymorphic deserialization for the sealed FileContent interface, + * which permits two implementations: + *

    + *
  • {@link FileWithBytes} - File content embedded as base64-encoded bytes
  • + *
  • {@link FileWithUri} - File content referenced by URI
  • + *
+ *

+ * The adapter distinguishes between the two types by checking for the presence of + * "bytes" or "uri" fields in the JSON object. + * + * @see FileContent + * @see FileWithBytes + * @see FileWithUri + */ + static class FileContentTypeAdapter extends TypeAdapter { + + // Create separate Gson instance without the FileContent adapter to avoid recursion + private final Gson delegateGson = new Gson(); + + @Override + public void write(JsonWriter out, FileContent value) throws java.io.IOException { + if (value == null) { + out.nullValue(); + return; + } + // Delegate to Gson's default serialization for the concrete type + if (value instanceof FileWithBytes fileWithBytes) { + delegateGson.toJson(fileWithBytes, FileWithBytes.class, out); + } else if (value instanceof FileWithUri fileWithUri) { + delegateGson.toJson(fileWithUri, FileWithUri.class, out); + } else { + throw new JsonSyntaxException("Unknown FileContent implementation: " + value.getClass().getName()); + } + } + + @Override + public @Nullable + FileContent read(JsonReader in) throws java.io.IOException { + if (in.peek() == com.google.gson.stream.JsonToken.NULL) { + in.nextNull(); + return null; + } + + // Read the JSON as a tree to inspect the fields + com.google.gson.JsonElement jsonElement = com.google.gson.JsonParser.parseReader(in); + if (!jsonElement.isJsonObject()) { + throw new JsonSyntaxException("FileContent must be a JSON object"); + } + + com.google.gson.JsonObject jsonObject = jsonElement.getAsJsonObject(); + + // Distinguish between FileWithBytes and FileWithUri by checking for "bytes" or "uri" field + if (jsonObject.has("bytes")) { + return delegateGson.fromJson(jsonElement, FileWithBytes.class); + } else if (jsonObject.has("uri")) { + return delegateGson.fromJson(jsonElement, FileWithUri.class); + } else { + throw new JsonSyntaxException("FileContent must have either 'bytes' or 'uri' field"); + } + } + } + +} diff --git a/spec/src/main/java/io/a2a/json/package-info.java b/spec/src/main/java/io/a2a/json/package-info.java new file mode 100644 index 000000000..81fe05a3a --- /dev/null +++ b/spec/src/main/java/io/a2a/json/package-info.java @@ -0,0 +1,8 @@ +/** + * JSON processing exceptions for the A2A Java SDK. + *

+ * This package provides custom exceptions that replace Jackson's JSON processing exceptions, + * allowing the SDK to be independent of any specific JSON library implementation. + */ +@org.jspecify.annotations.NullMarked +package io.a2a.json; diff --git a/spec/src/main/java/io/a2a/spec/A2AClientJSONError.java b/spec/src/main/java/io/a2a/spec/A2AClientJSONError.java index 75988da1c..06d04605c 100644 --- a/spec/src/main/java/io/a2a/spec/A2AClientJSONError.java +++ b/spec/src/main/java/io/a2a/spec/A2AClientJSONError.java @@ -1,5 +1,30 @@ package io.a2a.spec; +/** + * Client exception indicating a JSON serialization or deserialization error. + *

+ * This exception is thrown when the A2A client SDK encounters errors while + * parsing JSON responses from agents or serializing requests. This typically + * indicates: + *

    + *
  • Malformed JSON in agent responses
  • + *
  • Unexpected JSON structure or field types
  • + *
  • Missing required JSON fields
  • + *
  • JSON encoding/decoding errors
  • + *
+ *

+ * Usage example: + *

{@code
+ * try {
+ *     AgentCard card = objectMapper.readValue(json, AgentCard.class);
+ * } catch (io.a2a.json.JsonProcessingException e) {
+ *     throw new A2AClientJSONError("Failed to parse agent card", e);
+ * }
+ * }
+ * + * @see A2AClientError for the base client error class + * @see JSONParseError for protocol-level JSON errors + */ public class A2AClientJSONError extends A2AClientError { public A2AClientJSONError() { diff --git a/spec/src/main/java/io/a2a/spec/A2AErrorCodes.java b/spec/src/main/java/io/a2a/spec/A2AErrorCodes.java new file mode 100644 index 000000000..09175b744 --- /dev/null +++ b/spec/src/main/java/io/a2a/spec/A2AErrorCodes.java @@ -0,0 +1,22 @@ +package io.a2a.spec; + +/** + * All the error codes for A2A errors. + */ +public interface A2AErrorCodes { + + int TASK_NOT_FOUND_ERROR_CODE = -32001; + int TASK_NOT_CANCELABLE_ERROR_CODE = -32002; + int PUSH_NOTIFICATION_NOT_SUPPORTED_ERROR_CODE = -32003; + int UNSUPPORTED_OPERATION_ERROR_CODE = -32004; + int CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE = -32005; + int INVALID_AGENT_RESPONSE_ERROR_CODE = -32006; + int AUTHENTICATED_EXTENDED_CARD_NOT_CONFIGURED_ERROR_CODE = -32007; + + int INVALID_REQUEST_ERROR_CODE = -32600; + int METHOD_NOT_FOUND_ERROR_CODE = -32601; + int INVALID_PARAMS_ERROR_CODE = -32602; + int INTERNAL_ERROR_CODE = -32603; + + int JSON_PARSE_ERROR_CODE = -32700; +} diff --git a/spec/src/main/java/io/a2a/spec/APIKeySecurityScheme.java b/spec/src/main/java/io/a2a/spec/APIKeySecurityScheme.java index 028fea303..a2dc48c4d 100644 --- a/spec/src/main/java/io/a2a/spec/APIKeySecurityScheme.java +++ b/spec/src/main/java/io/a2a/spec/APIKeySecurityScheme.java @@ -1,12 +1,5 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; -import com.fasterxml.jackson.annotation.JsonValue; - import io.a2a.util.Assert; import static io.a2a.spec.APIKeySecurityScheme.API_KEY; @@ -14,9 +7,6 @@ /** * Defines a security scheme using an API key. */ -@JsonTypeName(API_KEY) -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class APIKeySecurityScheme implements SecurityScheme { public static final String API_KEY = "apiKey"; @@ -39,12 +29,10 @@ public enum Location { this.location = location; } - @JsonValue public String asString() { return location; } - @JsonCreator public static Location fromString(String location) { switch (location) { case "cookie" -> { @@ -65,9 +53,8 @@ public APIKeySecurityScheme(String in, String name, String description) { this(in, name, description, API_KEY); } - @JsonCreator - public APIKeySecurityScheme(@JsonProperty("in") String in, @JsonProperty("name") String name, - @JsonProperty("description") String description, @JsonProperty("type") String type) { + public APIKeySecurityScheme(String in, String name, + String description, String type) { Assert.checkNotNullParam("in", in); Assert.checkNotNullParam("name", name); Assert.checkNotNullParam("type", type); diff --git a/spec/src/main/java/io/a2a/spec/AgentCapabilities.java b/spec/src/main/java/io/a2a/spec/AgentCapabilities.java index 1c6fdf1b2..1de51d5f9 100644 --- a/spec/src/main/java/io/a2a/spec/AgentCapabilities.java +++ b/spec/src/main/java/io/a2a/spec/AgentCapabilities.java @@ -2,14 +2,9 @@ import java.util.List; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; - /** * Defines optional capabilities supported by an agent. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record AgentCapabilities(boolean streaming, boolean pushNotifications, boolean stateTransitionHistory, List extensions) { diff --git a/spec/src/main/java/io/a2a/spec/AgentCard.java b/spec/src/main/java/io/a2a/spec/AgentCard.java index b59a9403b..2574f5425 100644 --- a/spec/src/main/java/io/a2a/spec/AgentCard.java +++ b/spec/src/main/java/io/a2a/spec/AgentCard.java @@ -4,8 +4,6 @@ import java.util.List; import java.util.Map; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; import io.a2a.util.Assert; /** @@ -13,8 +11,6 @@ * metadata including the agent's identity, capabilities, skills, supported * communication methods, and security requirements. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record AgentCard(String name, String description, String url, AgentProvider provider, String version, String documentationUrl, AgentCapabilities capabilities, List defaultInputModes, List defaultOutputModes, List skills, diff --git a/spec/src/main/java/io/a2a/spec/AgentCardSignature.java b/spec/src/main/java/io/a2a/spec/AgentCardSignature.java index 4e383d998..70a92cd57 100644 --- a/spec/src/main/java/io/a2a/spec/AgentCardSignature.java +++ b/spec/src/main/java/io/a2a/spec/AgentCardSignature.java @@ -1,20 +1,15 @@ package io.a2a.spec; +import com.google.gson.annotations.SerializedName; import java.util.Map; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - import io.a2a.util.Assert; /** * Represents a JWS signature of an AgentCard. * This follows the JSON format of an RFC 7515 JSON Web Signature (JWS). */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) -public record AgentCardSignature(Map header, @JsonProperty("protected") String protectedHeader, +public record AgentCardSignature(Map header, @SerializedName("protected")String protectedHeader, String signature) { public AgentCardSignature { diff --git a/spec/src/main/java/io/a2a/spec/AgentInterface.java b/spec/src/main/java/io/a2a/spec/AgentInterface.java index db81ce8f0..0b2e8d8b0 100644 --- a/spec/src/main/java/io/a2a/spec/AgentInterface.java +++ b/spec/src/main/java/io/a2a/spec/AgentInterface.java @@ -1,16 +1,13 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; + import io.a2a.util.Assert; /** * Declares a combination of a target URL and a transport protocol for interacting with the agent. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) -public record AgentInterface(String transport, String url) { +public record AgentInterface(String transport, String url) { public AgentInterface { Assert.checkNotNullParam("transport", transport); Assert.checkNotNullParam("url", url); diff --git a/spec/src/main/java/io/a2a/spec/AgentProvider.java b/spec/src/main/java/io/a2a/spec/AgentProvider.java index fa57b4478..1d50b699e 100644 --- a/spec/src/main/java/io/a2a/spec/AgentProvider.java +++ b/spec/src/main/java/io/a2a/spec/AgentProvider.java @@ -1,14 +1,10 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; import io.a2a.util.Assert; /** * Represents the service provider of an agent. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record AgentProvider(String organization, String url) { public AgentProvider { diff --git a/spec/src/main/java/io/a2a/spec/AgentSkill.java b/spec/src/main/java/io/a2a/spec/AgentSkill.java index 3802b3418..b397f6248 100644 --- a/spec/src/main/java/io/a2a/spec/AgentSkill.java +++ b/spec/src/main/java/io/a2a/spec/AgentSkill.java @@ -3,15 +3,11 @@ import java.util.List; import java.util.Map; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; import io.a2a.util.Assert; /** * The set of skills, or distinct capabilities, that the agent can perform. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record AgentSkill(String id, String name, String description, List tags, List examples, List inputModes, List outputModes, List>> security) { diff --git a/spec/src/main/java/io/a2a/spec/Artifact.java b/spec/src/main/java/io/a2a/spec/Artifact.java index 798ac5823..69d2f0581 100644 --- a/spec/src/main/java/io/a2a/spec/Artifact.java +++ b/spec/src/main/java/io/a2a/spec/Artifact.java @@ -3,15 +3,11 @@ import java.util.List; import java.util.Map; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; import io.a2a.util.Assert; /** * Represents a file, data structure, or other resource generated by an agent during a task. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record Artifact(String artifactId, String name, String description, List> parts, Map metadata, List extensions) { diff --git a/spec/src/main/java/io/a2a/spec/AuthenticatedExtendedCardNotConfiguredError.java b/spec/src/main/java/io/a2a/spec/AuthenticatedExtendedCardNotConfiguredError.java index 323cd147d..7ac3e9d9c 100644 --- a/spec/src/main/java/io/a2a/spec/AuthenticatedExtendedCardNotConfiguredError.java +++ b/spec/src/main/java/io/a2a/spec/AuthenticatedExtendedCardNotConfiguredError.java @@ -2,34 +2,19 @@ import static io.a2a.util.Utils.defaultIfNull; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; /** * An A2A-specific error indicating that the agent does not have an * Authenticated Extended Card configured */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public class AuthenticatedExtendedCardNotConfiguredError extends JSONRPCError { public final static Integer DEFAULT_CODE = -32007; - @JsonCreator - public AuthenticatedExtendedCardNotConfiguredError( - @JsonProperty("code") Integer code, - @JsonProperty("message") String message, - @JsonProperty("data") Object data) { + public AuthenticatedExtendedCardNotConfiguredError(Integer code, String message, Object data) { super( defaultIfNull(code, DEFAULT_CODE), defaultIfNull(message, "Authenticated Extended Card not configured"), data); } - - public AuthenticatedExtendedCardNotConfiguredError() { - this(null, null, null); - } - } diff --git a/spec/src/main/java/io/a2a/spec/AuthenticationInfo.java b/spec/src/main/java/io/a2a/spec/AuthenticationInfo.java index d28a1e173..4f24e3c4c 100644 --- a/spec/src/main/java/io/a2a/spec/AuthenticationInfo.java +++ b/spec/src/main/java/io/a2a/spec/AuthenticationInfo.java @@ -2,15 +2,11 @@ import java.util.List; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; import io.a2a.util.Assert; /** * The authentication info for an agent. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record AuthenticationInfo(List schemes, String credentials) { public AuthenticationInfo { diff --git a/spec/src/main/java/io/a2a/spec/AuthorizationCodeOAuthFlow.java b/spec/src/main/java/io/a2a/spec/AuthorizationCodeOAuthFlow.java index afa6fed72..cc5e0ee54 100644 --- a/spec/src/main/java/io/a2a/spec/AuthorizationCodeOAuthFlow.java +++ b/spec/src/main/java/io/a2a/spec/AuthorizationCodeOAuthFlow.java @@ -2,16 +2,12 @@ import java.util.Map; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; import io.a2a.util.Assert; /** * Defines configuration details for the OAuth 2.0 Authorization Code flow. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record AuthorizationCodeOAuthFlow(String authorizationUrl, String refreshUrl, Map scopes, String tokenUrl) { diff --git a/spec/src/main/java/io/a2a/spec/CancelTaskRequest.java b/spec/src/main/java/io/a2a/spec/CancelTaskRequest.java index 39c370ae3..f8a1bb8bf 100644 --- a/spec/src/main/java/io/a2a/spec/CancelTaskRequest.java +++ b/spec/src/main/java/io/a2a/spec/CancelTaskRequest.java @@ -4,25 +4,17 @@ import java.util.UUID; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; import io.a2a.util.Assert; /** * A request that can be used to cancel a task. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class CancelTaskRequest extends NonStreamingJSONRPCRequest { public static final String METHOD = "tasks/cancel"; - @JsonCreator - public CancelTaskRequest(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("method") String method, @JsonProperty("params") TaskIdParams params) { + public CancelTaskRequest(String jsonrpc, Object id, String method, TaskIdParams params) { if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); } diff --git a/spec/src/main/java/io/a2a/spec/CancelTaskResponse.java b/spec/src/main/java/io/a2a/spec/CancelTaskResponse.java index 9ef775118..d65091080 100644 --- a/spec/src/main/java/io/a2a/spec/CancelTaskResponse.java +++ b/spec/src/main/java/io/a2a/spec/CancelTaskResponse.java @@ -1,20 +1,12 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - /** * A response to a cancel task request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) + public final class CancelTaskResponse extends JSONRPCResponse { - @JsonCreator - public CancelTaskResponse(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("result") Task result, @JsonProperty("error") JSONRPCError error) { + public CancelTaskResponse(String jsonrpc, Object id, Task result, JSONRPCError error) { super(jsonrpc, id, result, error, Task.class); } diff --git a/spec/src/main/java/io/a2a/spec/ClientCredentialsOAuthFlow.java b/spec/src/main/java/io/a2a/spec/ClientCredentialsOAuthFlow.java index 372b57d78..18056681f 100644 --- a/spec/src/main/java/io/a2a/spec/ClientCredentialsOAuthFlow.java +++ b/spec/src/main/java/io/a2a/spec/ClientCredentialsOAuthFlow.java @@ -3,16 +3,12 @@ import java.util.Map; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; import io.a2a.util.Assert; /** * Defines configuration details for the OAuth 2.0 Client Credentials flow. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record ClientCredentialsOAuthFlow(String refreshUrl, Map scopes, String tokenUrl) { public ClientCredentialsOAuthFlow { diff --git a/spec/src/main/java/io/a2a/spec/ContentTypeNotSupportedError.java b/spec/src/main/java/io/a2a/spec/ContentTypeNotSupportedError.java index 3aa245ca2..fc8c412d5 100644 --- a/spec/src/main/java/io/a2a/spec/ContentTypeNotSupportedError.java +++ b/spec/src/main/java/io/a2a/spec/ContentTypeNotSupportedError.java @@ -1,29 +1,19 @@ package io.a2a.spec; +import static io.a2a.spec.A2AErrorCodes.CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE; import static io.a2a.util.Utils.defaultIfNull; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; /** * An A2A-specific error indicating an incompatibility between the requested * content types and the agent's capabilities. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public class ContentTypeNotSupportedError extends JSONRPCError { - public final static Integer DEFAULT_CODE = -32005; + public final static Integer DEFAULT_CODE = CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE; - @JsonCreator - public ContentTypeNotSupportedError( - @JsonProperty("code") Integer code, - @JsonProperty("message") String message, - @JsonProperty("data") Object data) { - super( - defaultIfNull(code, DEFAULT_CODE), + public ContentTypeNotSupportedError(Integer code, String message, Object data) { + super(defaultIfNull(code, CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE), defaultIfNull(message, "Incompatible content types"), data); } diff --git a/spec/src/main/java/io/a2a/spec/DataPart.java b/spec/src/main/java/io/a2a/spec/DataPart.java index 7ac244263..58e8ae412 100644 --- a/spec/src/main/java/io/a2a/spec/DataPart.java +++ b/spec/src/main/java/io/a2a/spec/DataPart.java @@ -2,21 +2,12 @@ import java.util.Map; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; import io.a2a.util.Assert; -import static io.a2a.spec.DataPart.DATA; /** * Represents a structured data segment (e.g., JSON) within a message or artifact. */ -@JsonTypeName(DATA) -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public class DataPart extends Part> { public static final String DATA = "data"; @@ -28,9 +19,7 @@ public DataPart(Map data) { this(data, null); } - @JsonCreator - public DataPart(@JsonProperty("data") Map data, - @JsonProperty("metadata") Map metadata) { + public DataPart(Map data, Map metadata) { Assert.checkNotNullParam("data", data); this.data = data; this.metadata = metadata; diff --git a/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigParams.java b/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigParams.java index a64421a4c..0cb34a38d 100644 --- a/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigParams.java +++ b/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigParams.java @@ -2,16 +2,12 @@ import java.util.Map; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; import io.a2a.util.Assert; /** * Parameters for removing pushNotificationConfiguration associated with a Task. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record DeleteTaskPushNotificationConfigParams(String id, String pushNotificationConfigId, Map metadata) { public DeleteTaskPushNotificationConfigParams { diff --git a/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigRequest.java b/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigRequest.java index 99f50ebfd..dc3449ada 100644 --- a/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigRequest.java +++ b/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigRequest.java @@ -2,27 +2,17 @@ import java.util.UUID; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - import io.a2a.util.Assert; import io.a2a.util.Utils; /** * A delete task push notification config request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class DeleteTaskPushNotificationConfigRequest extends NonStreamingJSONRPCRequest { public static final String METHOD = "tasks/pushNotificationConfig/delete"; - @JsonCreator - public DeleteTaskPushNotificationConfigRequest(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("method") String method, - @JsonProperty("params") DeleteTaskPushNotificationConfigParams params) { + public DeleteTaskPushNotificationConfigRequest(String jsonrpc, Object id, String method, DeleteTaskPushNotificationConfigParams params) { if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); } diff --git a/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigResponse.java b/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigResponse.java index 0f65b5ad5..89e4964db 100644 --- a/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigResponse.java +++ b/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigResponse.java @@ -1,23 +1,11 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; - /** * A response for a delete task push notification config request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) -@JsonSerialize(using = JSONRPCVoidResponseSerializer.class) public final class DeleteTaskPushNotificationConfigResponse extends JSONRPCResponse { - @JsonCreator - public DeleteTaskPushNotificationConfigResponse(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("result") Void result, - @JsonProperty("error") JSONRPCError error) { + public DeleteTaskPushNotificationConfigResponse(String jsonrpc, Object id, Void result,JSONRPCError error) { super(jsonrpc, id, result, error, Void.class); } diff --git a/spec/src/main/java/io/a2a/spec/EventKind.java b/spec/src/main/java/io/a2a/spec/EventKind.java index a1ed7ef31..888611f81 100644 --- a/spec/src/main/java/io/a2a/spec/EventKind.java +++ b/spec/src/main/java/io/a2a/spec/EventKind.java @@ -1,21 +1,24 @@ package io.a2a.spec; -import static io.a2a.spec.Message.MESSAGE; -import static io.a2a.spec.Task.TASK; - -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; - -@JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "kind", - visible = true -) -@JsonSubTypes({ - @JsonSubTypes.Type(value = Task.class, name = TASK), - @JsonSubTypes.Type(value = Message.class, name = MESSAGE) -}) +/** + * Interface for events that can be returned from non-streaming A2A Protocol operations. + *

+ * EventKind represents events that are suitable for synchronous request-response patterns. + * These events provide complete state information and are typically returned as the final + * result of an operation. + *

+ * EventKind implementations use polymorphic JSON serialization with the "kind" discriminator + * to determine the concrete type during deserialization. + *

+ * Permitted implementations: + *

    + *
  • {@link Task} - Complete task state with status and artifacts
  • + *
  • {@link Message} - Full message with all content parts
  • + *
+ * + * @see StreamingEventKind + * @see Event + */ public interface EventKind { String getKind(); diff --git a/spec/src/main/java/io/a2a/spec/FileContent.java b/spec/src/main/java/io/a2a/spec/FileContent.java index f9609fb8b..c1d631e5c 100644 --- a/spec/src/main/java/io/a2a/spec/FileContent.java +++ b/spec/src/main/java/io/a2a/spec/FileContent.java @@ -1,8 +1,29 @@ package io.a2a.spec; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; - -@JsonDeserialize(using = FileContentDeserializer.class) +/** + * Sealed interface representing file content in the A2A Protocol. + *

+ * FileContent provides a polymorphic abstraction for file data, allowing files to be + * represented either as embedded binary content or as URI references. This flexibility + * enables different strategies for file transmission based on size, security, and + * accessibility requirements. + *

+ * The sealed interface permits only two implementations: + *

    + *
  • {@link FileWithBytes} - File content embedded as base64-encoded bytes (for small files or inline data)
  • + *
  • {@link FileWithUri} - File content referenced by URI (for large files or external resources)
  • + *
+ *

+ * Both implementations must provide: + *

    + *
  • MIME type - Describes the file format (e.g., "image/png", "application/pdf")
  • + *
  • File name - The original or display name for the file
  • + *
+ * + * @see FilePart + * @see FileWithBytes + * @see FileWithUri + */ public sealed interface FileContent permits FileWithBytes, FileWithUri { String mimeType(); diff --git a/spec/src/main/java/io/a2a/spec/FileContentDeserializer.java b/spec/src/main/java/io/a2a/spec/FileContentDeserializer.java deleted file mode 100644 index aa763db42..000000000 --- a/spec/src/main/java/io/a2a/spec/FileContentDeserializer.java +++ /dev/null @@ -1,38 +0,0 @@ -package io.a2a.spec; - -import java.io.IOException; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; - -public class FileContentDeserializer extends StdDeserializer { - - public FileContentDeserializer() { - this(null); - } - - public FileContentDeserializer(Class vc) { - super(vc); - } - - @Override - public FileContent deserialize(JsonParser jsonParser, DeserializationContext context) - throws IOException, JsonProcessingException { - JsonNode node = jsonParser.getCodec().readTree(jsonParser); - JsonNode mimeType = node.get("mimeType"); - JsonNode name = node.get("name"); - JsonNode bytes = node.get("bytes"); - if (bytes != null) { - return new FileWithBytes(mimeType != null ? mimeType.asText() : null, - name != null ? name.asText() : null, bytes.asText()); - } else if (node.has("uri")) { - return new FileWithUri(mimeType != null ? mimeType.asText() : null, - name != null ? name.asText() : null, node.get("uri").asText()); - } else { - throw new IOException("Invalid file format: missing 'bytes' or 'uri'"); - } - } -} diff --git a/spec/src/main/java/io/a2a/spec/FilePart.java b/spec/src/main/java/io/a2a/spec/FilePart.java index 79bd2e5e8..fae1788e3 100644 --- a/spec/src/main/java/io/a2a/spec/FilePart.java +++ b/spec/src/main/java/io/a2a/spec/FilePart.java @@ -2,11 +2,6 @@ import java.util.Map; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; import io.a2a.util.Assert; import static io.a2a.spec.FilePart.FILE; @@ -15,9 +10,6 @@ * Represents a file segment within a message or artifact. The file content can be * provided either directly as bytes or as a URI. */ -@JsonTypeName(FILE) -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public class FilePart extends Part { public static final String FILE = "file"; @@ -29,8 +21,7 @@ public FilePart(FileContent file) { this(file, null); } - @JsonCreator - public FilePart(@JsonProperty("file") FileContent file, @JsonProperty("metadata") Map metadata) { + public FilePart(FileContent file, Map metadata) { Assert.checkNotNullParam("file", file); this.file = file; this.metadata = metadata; diff --git a/spec/src/main/java/io/a2a/spec/FileWithBytes.java b/spec/src/main/java/io/a2a/spec/FileWithBytes.java index 782bc2c02..01ccef127 100644 --- a/spec/src/main/java/io/a2a/spec/FileWithBytes.java +++ b/spec/src/main/java/io/a2a/spec/FileWithBytes.java @@ -1,12 +1,7 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; - /** * Represents a file with its content provided directly as a base64-encoded string. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record FileWithBytes(String mimeType, String name, String bytes) implements FileContent { } diff --git a/spec/src/main/java/io/a2a/spec/FileWithUri.java b/spec/src/main/java/io/a2a/spec/FileWithUri.java index afb3a87d8..e1edd4bd2 100644 --- a/spec/src/main/java/io/a2a/spec/FileWithUri.java +++ b/spec/src/main/java/io/a2a/spec/FileWithUri.java @@ -1,13 +1,8 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; - /** * Represents a file with its content located at a specific URI. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record FileWithUri(String mimeType, String name, String uri) implements FileContent { } diff --git a/spec/src/main/java/io/a2a/spec/GetAuthenticatedExtendedCardRequest.java b/spec/src/main/java/io/a2a/spec/GetAuthenticatedExtendedCardRequest.java index 9d561c2d2..4afe2d029 100644 --- a/spec/src/main/java/io/a2a/spec/GetAuthenticatedExtendedCardRequest.java +++ b/spec/src/main/java/io/a2a/spec/GetAuthenticatedExtendedCardRequest.java @@ -2,10 +2,6 @@ import java.util.UUID; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; import io.a2a.util.Assert; import io.a2a.util.Utils; @@ -13,15 +9,11 @@ /** * Represents a JSON-RPC request for the `agent/getAuthenticatedExtendedCard` method. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class GetAuthenticatedExtendedCardRequest extends NonStreamingJSONRPCRequest { public static final String METHOD = "agent/getAuthenticatedExtendedCard"; - @JsonCreator - public GetAuthenticatedExtendedCardRequest(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("method") String method, @JsonProperty("params") Void params) { + public GetAuthenticatedExtendedCardRequest(String jsonrpc, Object id, String method, Void params) { if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); } diff --git a/spec/src/main/java/io/a2a/spec/GetAuthenticatedExtendedCardResponse.java b/spec/src/main/java/io/a2a/spec/GetAuthenticatedExtendedCardResponse.java index ec624e77d..48f380f12 100644 --- a/spec/src/main/java/io/a2a/spec/GetAuthenticatedExtendedCardResponse.java +++ b/spec/src/main/java/io/a2a/spec/GetAuthenticatedExtendedCardResponse.java @@ -1,21 +1,11 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - /** * A response for the `agent/getAuthenticatedExtendedCard` method. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class GetAuthenticatedExtendedCardResponse extends JSONRPCResponse { - @JsonCreator - public GetAuthenticatedExtendedCardResponse(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("result") AgentCard result, - @JsonProperty("error") JSONRPCError error) { + public GetAuthenticatedExtendedCardResponse(String jsonrpc, Object id, AgentCard result, JSONRPCError error) { super(jsonrpc, id, result, error, AgentCard.class); } diff --git a/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigParams.java b/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigParams.java index 627feff34..2836e2065 100644 --- a/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigParams.java +++ b/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigParams.java @@ -2,8 +2,6 @@ import java.util.Map; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; import io.a2a.util.Assert; import org.jspecify.annotations.Nullable; @@ -11,8 +9,6 @@ /** * Parameters for fetching a pushNotificationConfiguration associated with a Task. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record GetTaskPushNotificationConfigParams(String id, @Nullable String pushNotificationConfigId, @Nullable Map metadata) { public GetTaskPushNotificationConfigParams { diff --git a/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigRequest.java b/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigRequest.java index b353e0cc8..4f1ef9e88 100644 --- a/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigRequest.java +++ b/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigRequest.java @@ -1,9 +1,5 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; import io.a2a.util.Assert; import io.a2a.util.Utils; @@ -12,15 +8,11 @@ /** * A get task push notification request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class GetTaskPushNotificationConfigRequest extends NonStreamingJSONRPCRequest { public static final String METHOD = "tasks/pushNotificationConfig/get"; - @JsonCreator - public GetTaskPushNotificationConfigRequest(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("method") String method, @JsonProperty("params") GetTaskPushNotificationConfigParams params) { + public GetTaskPushNotificationConfigRequest( String jsonrpc, Object id, String method, GetTaskPushNotificationConfigParams params) { if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); } diff --git a/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigResponse.java b/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigResponse.java index 116799a9e..83a125925 100644 --- a/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigResponse.java +++ b/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigResponse.java @@ -1,21 +1,11 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - /** * A response for a get task push notification request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class GetTaskPushNotificationConfigResponse extends JSONRPCResponse { - @JsonCreator - public GetTaskPushNotificationConfigResponse(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("result") TaskPushNotificationConfig result, - @JsonProperty("error") JSONRPCError error) { + public GetTaskPushNotificationConfigResponse(String jsonrpc, Object id, TaskPushNotificationConfig result, JSONRPCError error) { super(jsonrpc, id, result, error, TaskPushNotificationConfig.class); } diff --git a/spec/src/main/java/io/a2a/spec/GetTaskRequest.java b/spec/src/main/java/io/a2a/spec/GetTaskRequest.java index f31237af0..7b7fb8803 100644 --- a/spec/src/main/java/io/a2a/spec/GetTaskRequest.java +++ b/spec/src/main/java/io/a2a/spec/GetTaskRequest.java @@ -4,25 +4,17 @@ import java.util.UUID; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; import io.a2a.util.Assert; /** * A get task request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class GetTaskRequest extends NonStreamingJSONRPCRequest { public static final String METHOD = "tasks/get"; - @JsonCreator - public GetTaskRequest(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("method") String method, @JsonProperty("params") TaskQueryParams params) { + public GetTaskRequest(String jsonrpc, Object id, String method, TaskQueryParams params) { if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); } diff --git a/spec/src/main/java/io/a2a/spec/GetTaskResponse.java b/spec/src/main/java/io/a2a/spec/GetTaskResponse.java index 0d27a8e68..f5e6fa2ee 100644 --- a/spec/src/main/java/io/a2a/spec/GetTaskResponse.java +++ b/spec/src/main/java/io/a2a/spec/GetTaskResponse.java @@ -1,20 +1,11 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - /** * The response for a get task request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class GetTaskResponse extends JSONRPCResponse { - @JsonCreator - public GetTaskResponse(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("result") Task result, @JsonProperty("error") JSONRPCError error) { + public GetTaskResponse(String jsonrpc, Object id, Task result, JSONRPCError error) { super(jsonrpc, id, result, error, Task.class); } diff --git a/spec/src/main/java/io/a2a/spec/HTTPAuthSecurityScheme.java b/spec/src/main/java/io/a2a/spec/HTTPAuthSecurityScheme.java index 408fd2605..0323d3fa9 100644 --- a/spec/src/main/java/io/a2a/spec/HTTPAuthSecurityScheme.java +++ b/spec/src/main/java/io/a2a/spec/HTTPAuthSecurityScheme.java @@ -1,11 +1,5 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -import com.fasterxml.jackson.annotation.JsonTypeName; import io.a2a.util.Assert; import static io.a2a.spec.HTTPAuthSecurityScheme.HTTP; @@ -13,9 +7,6 @@ /** * Defines a security scheme using HTTP authentication. */ -@JsonTypeName(HTTP) -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class HTTPAuthSecurityScheme implements SecurityScheme { public static final String HTTP = "http"; @@ -28,12 +19,10 @@ public HTTPAuthSecurityScheme(String bearerFormat, String scheme, String descrip this(bearerFormat, scheme, description, HTTP); } - @JsonCreator - public HTTPAuthSecurityScheme(@JsonProperty("bearerFormat") String bearerFormat, @JsonProperty("scheme") String scheme, - @JsonProperty("description") String description, @JsonProperty("type") String type) { + public HTTPAuthSecurityScheme(String bearerFormat, String scheme, String description, String type) { Assert.checkNotNullParam("scheme", scheme); Assert.checkNotNullParam("type", type); - if (! type.equals(HTTP)) { + if (! HTTP.equals(type)) { throw new IllegalArgumentException("Invalid type for HTTPAuthSecurityScheme"); } this.bearerFormat = bearerFormat; diff --git a/spec/src/main/java/io/a2a/spec/IdJsonMappingException.java b/spec/src/main/java/io/a2a/spec/IdJsonMappingException.java index 15e0b07b1..00270f909 100644 --- a/spec/src/main/java/io/a2a/spec/IdJsonMappingException.java +++ b/spec/src/main/java/io/a2a/spec/IdJsonMappingException.java @@ -1,18 +1,18 @@ package io.a2a.spec; -import com.fasterxml.jackson.databind.JsonMappingException; +import io.a2a.json.JsonMappingException; public class IdJsonMappingException extends JsonMappingException { Object id; public IdJsonMappingException(String msg, Object id) { - super(null, msg); + super(msg); this.id = id; } public IdJsonMappingException(String msg, Throwable cause, Object id) { - super(null, msg, cause); + super(msg, cause); this.id = id; } diff --git a/spec/src/main/java/io/a2a/spec/ImplicitOAuthFlow.java b/spec/src/main/java/io/a2a/spec/ImplicitOAuthFlow.java index 8e2d529ea..cd2ef6235 100644 --- a/spec/src/main/java/io/a2a/spec/ImplicitOAuthFlow.java +++ b/spec/src/main/java/io/a2a/spec/ImplicitOAuthFlow.java @@ -2,16 +2,12 @@ import java.util.Map; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; import io.a2a.util.Assert; /** * Defines configuration details for the OAuth 2.0 Implicit flow. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record ImplicitOAuthFlow(String authorizationUrl, String refreshUrl, Map scopes) { public ImplicitOAuthFlow { diff --git a/spec/src/main/java/io/a2a/spec/InternalError.java b/spec/src/main/java/io/a2a/spec/InternalError.java index ae52ecb70..f319c43ea 100644 --- a/spec/src/main/java/io/a2a/spec/InternalError.java +++ b/spec/src/main/java/io/a2a/spec/InternalError.java @@ -2,27 +2,17 @@ import static io.a2a.util.Utils.defaultIfNull; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; /** * An error indicating an internal error on the server. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public class InternalError extends JSONRPCError { - public final static Integer DEFAULT_CODE = -32603; + public final static Integer DEFAULT_CODE = A2AErrorCodes.INTERNAL_ERROR_CODE; - @JsonCreator - public InternalError( - @JsonProperty("code") Integer code, - @JsonProperty("message") String message, - @JsonProperty("data") Object data) { + public InternalError(Integer code, String message, Object data) { super( - defaultIfNull(code, DEFAULT_CODE), + defaultIfNull(code, A2AErrorCodes.INTERNAL_ERROR_CODE), defaultIfNull(message, "Internal Error"), data); } diff --git a/spec/src/main/java/io/a2a/spec/InvalidAgentResponseError.java b/spec/src/main/java/io/a2a/spec/InvalidAgentResponseError.java index faab71a96..d96e2d870 100644 --- a/spec/src/main/java/io/a2a/spec/InvalidAgentResponseError.java +++ b/spec/src/main/java/io/a2a/spec/InvalidAgentResponseError.java @@ -2,28 +2,48 @@ import static io.a2a.util.Utils.defaultIfNull; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; /** - * An A2A-specific error indicating that the agent returned a response that - * does not conform to the specification for the current method. + * A2A Protocol error indicating that an agent returned a response not conforming to protocol specifications. + *

+ * This error is typically raised by client implementations when validating agent responses. + * It indicates that the agent's response structure, content, or format violates the A2A Protocol + * requirements for the invoked method. + *

+ * Common violations: + *

    + *
  • Missing required fields in response objects
  • + *
  • Invalid field types or values
  • + *
  • Malformed event stream data
  • + *
  • Response doesn't match declared agent capabilities
  • + *
+ *

+ * Corresponds to A2A-specific error code {@code -32006}. + *

+ * Usage example: + *

{@code
+ * SendMessageResponse response = client.sendMessage(request);
+ * if (response.task() == null) {
+ *     throw new InvalidAgentResponseError(
+ *         null,
+ *         "Response missing required 'task' field",
+ *         null
+ *     );
+ * }
+ * }
+ * + * @see JSONRPCResponse for response structure + * @see SendMessageResponse for message send response + * @see A2A Protocol Specification + */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public class InvalidAgentResponseError extends JSONRPCError { - public final static Integer DEFAULT_CODE = -32006; + public final static Integer DEFAULT_CODE = A2AErrorCodes.INVALID_AGENT_RESPONSE_ERROR_CODE; - @JsonCreator - public InvalidAgentResponseError( - @JsonProperty("code") Integer code, - @JsonProperty("message") String message, - @JsonProperty("data") Object data) { + public InvalidAgentResponseError(Integer code, String message, Object data) { super( - defaultIfNull(code, DEFAULT_CODE), + defaultIfNull(code, A2AErrorCodes.INVALID_AGENT_RESPONSE_ERROR_CODE), defaultIfNull(message, "Invalid agent response"), data); } diff --git a/spec/src/main/java/io/a2a/spec/InvalidParamsError.java b/spec/src/main/java/io/a2a/spec/InvalidParamsError.java index c71ea14bf..eb067eec5 100644 --- a/spec/src/main/java/io/a2a/spec/InvalidParamsError.java +++ b/spec/src/main/java/io/a2a/spec/InvalidParamsError.java @@ -2,27 +2,40 @@ import static io.a2a.util.Utils.defaultIfNull; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - /** - * An error indicating that the method parameters are invalid. + * JSON-RPC error indicating that method parameters are invalid or missing required fields. + *

+ * This error is returned when a JSON-RPC method is called with parameters that fail validation. + * Common causes include: + *

    + *
  • Missing required parameters
  • + *
  • Parameters of incorrect type
  • + *
  • Parameter values outside acceptable ranges
  • + *
  • Malformed parameter structures
  • + *
+ *

+ * Corresponds to JSON-RPC 2.0 error code {@code -32602}. + *

+ * Usage example: + *

{@code
+ * // Default error with standard message
+ * throw new InvalidParamsError();
+ *
+ * // Custom error message
+ * throw new InvalidParamsError("taskId parameter is required");
+ * }
+ * + * @see JSONRPCError for the base error class + * @see A2AError for the error marker interface + * @see JSON-RPC 2.0 Error Codes */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public class InvalidParamsError extends JSONRPCError { - public final static Integer DEFAULT_CODE = -32602; + public final static Integer DEFAULT_CODE = A2AErrorCodes.INVALID_PARAMS_ERROR_CODE; - @JsonCreator - public InvalidParamsError( - @JsonProperty("code") Integer code, - @JsonProperty("message") String message, - @JsonProperty("data") Object data) { + public InvalidParamsError(Integer code, String message, Object data) { super( - defaultIfNull(code, DEFAULT_CODE), + defaultIfNull(code, A2AErrorCodes.INVALID_PARAMS_ERROR_CODE), defaultIfNull(message, "Invalid parameters"), data); } diff --git a/spec/src/main/java/io/a2a/spec/InvalidRequestError.java b/spec/src/main/java/io/a2a/spec/InvalidRequestError.java index 4d9e50779..66d9ddb05 100644 --- a/spec/src/main/java/io/a2a/spec/InvalidRequestError.java +++ b/spec/src/main/java/io/a2a/spec/InvalidRequestError.java @@ -2,31 +2,45 @@ import static io.a2a.util.Utils.defaultIfNull; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; /** - * An error indicating that the JSON sent is not a valid Request object. + * JSON-RPC error indicating that the request payload is not a valid JSON-RPC Request object. + *

+ * This error is returned when the JSON-RPC request fails structural validation. + * Common causes include: + *

    + *
  • Missing required JSON-RPC fields (jsonrpc, method, id)
  • + *
  • Invalid JSON-RPC version (must be "2.0")
  • + *
  • Malformed request structure
  • + *
  • Type mismatches in required fields
  • + *
+ *

+ * Corresponds to JSON-RPC 2.0 error code {@code -32600}. + *

+ * Usage example: + *

{@code
+ * // Default error with standard message
+ * throw new InvalidRequestError();
+ *
+ * // Custom error message
+ * throw new InvalidRequestError("Missing 'method' field in request");
+ * }
+ * + * @see JSONRPCError for the base error class + * @see A2AError for the error marker interface + * @see JSON-RPC 2.0 Error Codes */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public class InvalidRequestError extends JSONRPCError { - public final static Integer DEFAULT_CODE = -32600; + public final static Integer DEFAULT_CODE = A2AErrorCodes.INVALID_REQUEST_ERROR_CODE; public InvalidRequestError() { this(null, null, null); } - @JsonCreator - public InvalidRequestError( - @JsonProperty("code") Integer code, - @JsonProperty("message") String message, - @JsonProperty("data") Object data) { + public InvalidRequestError(Integer code, String message, Object data) { super( - defaultIfNull(code, DEFAULT_CODE), + defaultIfNull(code, A2AErrorCodes.INVALID_REQUEST_ERROR_CODE), defaultIfNull(message, "Request payload validation error"), data); } diff --git a/spec/src/main/java/io/a2a/spec/JSONErrorResponse.java b/spec/src/main/java/io/a2a/spec/JSONErrorResponse.java index 3029f5394..8823f9bd3 100644 --- a/spec/src/main/java/io/a2a/spec/JSONErrorResponse.java +++ b/spec/src/main/java/io/a2a/spec/JSONErrorResponse.java @@ -1,9 +1,19 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; - -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) +/** + * A simplified error response wrapper for non-JSON-RPC error scenarios. + *

+ * This record provides a lightweight error response format for cases where + * a full JSON-RPC error structure is not appropriate, such as HTTP-level + * errors or transport-layer failures. + *

+ * Unlike {@link JSONRPCErrorResponse}, this is not part of the JSON-RPC 2.0 + * specification but serves as a utility for simpler error reporting in the + * A2A Java SDK implementation. + * + * @param error a human-readable error message + * @see JSONRPCErrorResponse + * @see JSONRPCError + */ public record JSONErrorResponse(String error) { } diff --git a/spec/src/main/java/io/a2a/spec/JSONParseError.java b/spec/src/main/java/io/a2a/spec/JSONParseError.java index d596c0701..acec50ca7 100644 --- a/spec/src/main/java/io/a2a/spec/JSONParseError.java +++ b/spec/src/main/java/io/a2a/spec/JSONParseError.java @@ -1,17 +1,31 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; import static io.a2a.util.Utils.defaultIfNull; /** - * An error indicating that the server received invalid JSON. + * JSON-RPC error indicating that the server received invalid JSON that could not be parsed. + *

+ * This error is returned when the request payload is not valid JSON, such as malformed syntax, + * unexpected tokens, or encoding issues. This is distinct from {@link InvalidRequestError}, + * which indicates structurally valid JSON that doesn't conform to the JSON-RPC specification. + *

+ * Corresponds to JSON-RPC 2.0 error code {@code -32700}. + *

+ * Usage example: + *

{@code
+ * try {
+ *     objectMapper.readValue(payload, JSONRPCRequest.class);
+ * } catch (io.a2a.json.JsonProcessingException e) {
+ *     throw new JSONParseError("Malformed JSON: " + e.getMessage());
+ * }
+ * }
+ * + * @see JSONRPCError for the base error class + * @see A2AError for the error marker interface + * @see InvalidRequestError for structurally valid but invalid requests + * @see JSON-RPC 2.0 Error Codes */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public class JSONParseError extends JSONRPCError implements A2AError { public final static Integer DEFAULT_CODE = -32700; @@ -24,11 +38,10 @@ public JSONParseError(String message) { this(null, message, null); } - @JsonCreator public JSONParseError( - @JsonProperty("code") Integer code, - @JsonProperty("message") String message, - @JsonProperty("data") Object data) { + Integer code, + String message, + Object data) { super( defaultIfNull(code, DEFAULT_CODE), defaultIfNull(message, "Invalid JSON payload"), diff --git a/spec/src/main/java/io/a2a/spec/JSONRPCError.java b/spec/src/main/java/io/a2a/spec/JSONRPCError.java index 776b9c8c6..766e54fda 100644 --- a/spec/src/main/java/io/a2a/spec/JSONRPCError.java +++ b/spec/src/main/java/io/a2a/spec/JSONRPCError.java @@ -1,31 +1,27 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; import io.a2a.util.Assert; /** * Represents a JSON-RPC 2.0 Error object, included in an error response. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonDeserialize(using = JSONRPCErrorDeserializer.class) -@JsonSerialize(using = JSONRPCErrorSerializer.class) -@JsonIgnoreProperties(ignoreUnknown = true) public class JSONRPCError extends Error implements Event, A2AError { private final Integer code; private final Object data; - @JsonCreator - public JSONRPCError( - @JsonProperty("code") Integer code, - @JsonProperty("message") String message, - @JsonProperty("data") Object data) { + /** + * Constructs a JSON-RPC error with the specified code, message, and optional data. + *

+ * This constructor is used by Jackson for JSON deserialization. + * + * @param code the numeric error code (required, see JSON-RPC 2.0 spec for standard codes) + * @param message the human-readable error message (required) + * @param data additional error information, structure defined by the error code (optional) + * @throws IllegalArgumentException if code or message is null + */ + public JSONRPCError(Integer code, String message, Object data) { super(message); Assert.checkNotNullParam("code", code); Assert.checkNotNullParam("message", message); diff --git a/spec/src/main/java/io/a2a/spec/JSONRPCErrorDeserializer.java b/spec/src/main/java/io/a2a/spec/JSONRPCErrorDeserializer.java deleted file mode 100644 index 229abf55d..000000000 --- a/spec/src/main/java/io/a2a/spec/JSONRPCErrorDeserializer.java +++ /dev/null @@ -1,59 +0,0 @@ -package io.a2a.spec; - -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; - -public class JSONRPCErrorDeserializer extends StdDeserializer { - - private static final Map> ERROR_MAP = new HashMap<>(); - - static { - ERROR_MAP.put(JSONParseError.DEFAULT_CODE, JSONParseError::new); - ERROR_MAP.put(InvalidRequestError.DEFAULT_CODE, InvalidRequestError::new); - ERROR_MAP.put(MethodNotFoundError.DEFAULT_CODE, MethodNotFoundError::new); - ERROR_MAP.put(InvalidParamsError.DEFAULT_CODE, InvalidParamsError::new); - ERROR_MAP.put(InternalError.DEFAULT_CODE, InternalError::new); - ERROR_MAP.put(PushNotificationNotSupportedError.DEFAULT_CODE, PushNotificationNotSupportedError::new); - ERROR_MAP.put(UnsupportedOperationError.DEFAULT_CODE, UnsupportedOperationError::new); - ERROR_MAP.put(ContentTypeNotSupportedError.DEFAULT_CODE, ContentTypeNotSupportedError::new); - ERROR_MAP.put(InvalidAgentResponseError.DEFAULT_CODE, InvalidAgentResponseError::new); - ERROR_MAP.put(TaskNotCancelableError.DEFAULT_CODE, TaskNotCancelableError::new); - ERROR_MAP.put(TaskNotFoundError.DEFAULT_CODE, TaskNotFoundError::new); - } - - public JSONRPCErrorDeserializer() { - this(null); - } - - public JSONRPCErrorDeserializer(Class vc) { - super(vc); - } - - @Override - public JSONRPCError deserialize(JsonParser jsonParser, DeserializationContext context) - throws IOException, JsonProcessingException { - JsonNode node = jsonParser.getCodec().readTree(jsonParser); - int code = node.get("code").asInt(); - String message = node.get("message").asText(); - JsonNode dataNode = node.get("data"); - Object data = dataNode != null ? jsonParser.getCodec().treeToValue(dataNode, Object.class) : null; - TriFunction constructor = ERROR_MAP.get(code); - if (constructor != null) { - return constructor.apply(code, message, data); - } else { - return new JSONRPCError(code, message, data); - } - } - - @FunctionalInterface - private interface TriFunction { - R apply(A a, B b, C c); - } -} diff --git a/spec/src/main/java/io/a2a/spec/JSONRPCErrorResponse.java b/spec/src/main/java/io/a2a/spec/JSONRPCErrorResponse.java index ea7846655..f24271c46 100644 --- a/spec/src/main/java/io/a2a/spec/JSONRPCErrorResponse.java +++ b/spec/src/main/java/io/a2a/spec/JSONRPCErrorResponse.java @@ -1,22 +1,24 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - import io.a2a.util.Assert; /** * A JSON RPC error response. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class JSONRPCErrorResponse extends JSONRPCResponse { - @JsonCreator - public JSONRPCErrorResponse(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("result") Void result, @JsonProperty("error") JSONRPCError error) { + /** + * Constructs a JSON-RPC error response with all fields. + *

+ * This constructor is used for JSON deserialization. + * + * @param jsonrpc the JSON-RPC version (must be "2.0") + * @param id the request ID, or null if the ID could not be determined from the request + * @param result must be null for error responses + * @param error the error object describing what went wrong (required) + * @throws IllegalArgumentException if error is null + */ + public JSONRPCErrorResponse(String jsonrpc, Object id, Void result, JSONRPCError error) { super(jsonrpc, id, result, error, Void.class); Assert.checkNotNullParam("error", error); } diff --git a/spec/src/main/java/io/a2a/spec/JSONRPCErrorSerializer.java b/spec/src/main/java/io/a2a/spec/JSONRPCErrorSerializer.java deleted file mode 100644 index 87b427548..000000000 --- a/spec/src/main/java/io/a2a/spec/JSONRPCErrorSerializer.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.a2a.spec; - -import java.io.IOException; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; - -public class JSONRPCErrorSerializer extends StdSerializer { - - public JSONRPCErrorSerializer() { - this(null); - } - - public JSONRPCErrorSerializer(Class vc) { - super(vc); - } - - @Override - public void serialize(JSONRPCError value, JsonGenerator gen, SerializerProvider provider) throws IOException { - gen.writeStartObject(); - gen.writeNumberField("code", value.getCode()); - gen.writeStringField("message", value.getMessage()); - if (value.getData() != null) { - gen.writeObjectField("data", value.getData()); - } - gen.writeEndObject(); - } -} diff --git a/spec/src/main/java/io/a2a/spec/JSONRPCRequest.java b/spec/src/main/java/io/a2a/spec/JSONRPCRequest.java index a88de90f1..45e2b6883 100644 --- a/spec/src/main/java/io/a2a/spec/JSONRPCRequest.java +++ b/spec/src/main/java/io/a2a/spec/JSONRPCRequest.java @@ -2,16 +2,11 @@ import static io.a2a.util.Utils.defaultIfNull; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; - import io.a2a.util.Assert; /** * Represents a JSONRPC request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public abstract sealed class JSONRPCRequest implements JSONRPCMessage permits NonStreamingJSONRPCRequest, StreamingJSONRPCRequest { protected String jsonrpc; diff --git a/spec/src/main/java/io/a2a/spec/JSONRPCRequestDeserializerBase.java b/spec/src/main/java/io/a2a/spec/JSONRPCRequestDeserializerBase.java deleted file mode 100644 index 2af7d0513..000000000 --- a/spec/src/main/java/io/a2a/spec/JSONRPCRequestDeserializerBase.java +++ /dev/null @@ -1,89 +0,0 @@ -package io.a2a.spec; - -import static io.a2a.util.Utils.OBJECT_MAPPER; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; - -public abstract class JSONRPCRequestDeserializerBase extends StdDeserializer> { - - public JSONRPCRequestDeserializerBase() { - this(null); - } - - public JSONRPCRequestDeserializerBase(Class vc) { - super(vc); - } - - protected T getAndValidateParams(JsonNode paramsNode, JsonParser jsonParser, JsonNode node, Class paramsType) throws JsonMappingException { - if (paramsNode == null) { - return null; - } - try { - return OBJECT_MAPPER.treeToValue(paramsNode, paramsType); - } catch (JsonProcessingException e) { - throw new InvalidParamsJsonMappingException("Invalid params", e, getIdIfPossible(node, jsonParser)); - } - } - - protected String getAndValidateJsonrpc(JsonNode treeNode, JsonParser jsonParser) throws JsonMappingException { - JsonNode jsonrpcNode = treeNode.get("jsonrpc"); - if (jsonrpcNode == null || ! jsonrpcNode.asText().equals(JSONRPCMessage.JSONRPC_VERSION)) { - throw new IdJsonMappingException("Invalid JSON-RPC protocol version", getIdIfPossible(treeNode, jsonParser)); - } - return jsonrpcNode.asText(); - } - - protected String getAndValidateMethod(JsonNode treeNode, JsonParser jsonParser) throws JsonMappingException { - JsonNode methodNode = treeNode.get("method"); - if (methodNode == null) { - throw new IdJsonMappingException("Missing method", getIdIfPossible(treeNode, jsonParser)); - } - String method = methodNode.asText(); - if (! isValidMethodName(method)) { - throw new MethodNotFoundJsonMappingException("Invalid method", getIdIfPossible(treeNode, jsonParser)); - } - return method; - } - - protected Object getAndValidateId(JsonNode treeNode, JsonParser jsonParser) throws JsonProcessingException { - JsonNode idNode = treeNode.get("id"); - Object id = null; - if (idNode != null) { - if (idNode.isTextual()) { - id = OBJECT_MAPPER.treeToValue(idNode, String.class); - } else if (idNode.isNumber()) { - id = OBJECT_MAPPER.treeToValue(idNode, Integer.class); - } else { - throw new JsonMappingException(jsonParser, "Invalid id"); - } - } - return id; - } - - protected Object getIdIfPossible(JsonNode treeNode, JsonParser jsonParser) { - try { - return getAndValidateId(treeNode, jsonParser); - } catch (JsonProcessingException e) { - // id can't be determined - return null; - } - } - - protected static boolean isValidMethodName(String methodName) { - return methodName != null && (methodName.equals(CancelTaskRequest.METHOD) - || methodName.equals(GetTaskRequest.METHOD) - || methodName.equals(GetTaskPushNotificationConfigRequest.METHOD) - || methodName.equals(SetTaskPushNotificationConfigRequest.METHOD) - || methodName.equals(TaskResubscriptionRequest.METHOD) - || methodName.equals(SendMessageRequest.METHOD) - || methodName.equals(SendStreamingMessageRequest.METHOD) - || methodName.equals(ListTaskPushNotificationConfigRequest.METHOD) - || methodName.equals(DeleteTaskPushNotificationConfigRequest.METHOD) - || methodName.equals(GetAuthenticatedExtendedCardRequest.METHOD)); - - } -} diff --git a/spec/src/main/java/io/a2a/spec/JSONRPCResponse.java b/spec/src/main/java/io/a2a/spec/JSONRPCResponse.java index be67a1e24..d4330d843 100644 --- a/spec/src/main/java/io/a2a/spec/JSONRPCResponse.java +++ b/spec/src/main/java/io/a2a/spec/JSONRPCResponse.java @@ -2,16 +2,11 @@ import static io.a2a.util.Utils.defaultIfNull; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; - import io.a2a.util.Assert; /** * Represents a JSONRPC response. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public abstract sealed class JSONRPCResponse implements JSONRPCMessage permits SendStreamingMessageResponse, GetTaskResponse, CancelTaskResponse, SetTaskPushNotificationConfigResponse, GetTaskPushNotificationConfigResponse, SendMessageResponse, DeleteTaskPushNotificationConfigResponse, ListTaskPushNotificationConfigResponse, JSONRPCErrorResponse, diff --git a/spec/src/main/java/io/a2a/spec/JSONRPCVoidResponseSerializer.java b/spec/src/main/java/io/a2a/spec/JSONRPCVoidResponseSerializer.java deleted file mode 100644 index 200bc4cd4..000000000 --- a/spec/src/main/java/io/a2a/spec/JSONRPCVoidResponseSerializer.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.a2a.spec; - -import java.io.IOException; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import com.fasterxml.jackson.databind.type.TypeFactory; - -public class JSONRPCVoidResponseSerializer extends StdSerializer> { - - private static final JSONRPCErrorSerializer JSON_RPC_ERROR_SERIALIZER = new JSONRPCErrorSerializer(); - - public JSONRPCVoidResponseSerializer() { - super(TypeFactory.defaultInstance().constructParametricType(JSONRPCResponse.class, - Void.class)); - } - - @Override - public void serialize(JSONRPCResponse value, JsonGenerator gen, SerializerProvider provider) throws IOException { - gen.writeStartObject(); - gen.writeStringField("jsonrpc", value.getJsonrpc()); - gen.writeObjectField("id", value.getId()); - if (value.getError() != null) { - gen.writeFieldName("error"); - JSON_RPC_ERROR_SERIALIZER.serialize(value.getError(), gen, provider); - } else { - gen.writeNullField("result"); - } - gen.writeEndObject(); - } -} diff --git a/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigParams.java b/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigParams.java index 5ebb12f76..2d04f36ce 100644 --- a/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigParams.java +++ b/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigParams.java @@ -2,16 +2,11 @@ import java.util.Map; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; - import io.a2a.util.Assert; /** * Parameters for getting list of pushNotificationConfigurations associated with a Task. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record ListTaskPushNotificationConfigParams(String id, Map metadata) { public ListTaskPushNotificationConfigParams { diff --git a/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigRequest.java b/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigRequest.java index 90ba0f1f5..8a4b75a1f 100644 --- a/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigRequest.java +++ b/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigRequest.java @@ -2,27 +2,17 @@ import java.util.UUID; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - import io.a2a.util.Assert; import io.a2a.util.Utils; /** * A list task push notification config request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class ListTaskPushNotificationConfigRequest extends NonStreamingJSONRPCRequest { public static final String METHOD = "tasks/pushNotificationConfig/list"; - @JsonCreator - public ListTaskPushNotificationConfigRequest(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("method") String method, - @JsonProperty("params") ListTaskPushNotificationConfigParams params) { + public ListTaskPushNotificationConfigRequest(String jsonrpc, Object id, String method, ListTaskPushNotificationConfigParams params) { if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); } diff --git a/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigResponse.java b/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigResponse.java index cc610416e..5546f148c 100644 --- a/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigResponse.java +++ b/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigResponse.java @@ -2,22 +2,12 @@ import java.util.List; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - /** * A response for a list task push notification config request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class ListTaskPushNotificationConfigResponse extends JSONRPCResponse> { - @JsonCreator - public ListTaskPushNotificationConfigResponse(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("result") List result, - @JsonProperty("error") JSONRPCError error) { + public ListTaskPushNotificationConfigResponse(String jsonrpc, Object id, List result, JSONRPCError error) { super(jsonrpc, id, result, error, (Class>) (Class) List.class); } diff --git a/spec/src/main/java/io/a2a/spec/Message.java b/spec/src/main/java/io/a2a/spec/Message.java index dd7e860a5..9d08cb56c 100644 --- a/spec/src/main/java/io/a2a/spec/Message.java +++ b/spec/src/main/java/io/a2a/spec/Message.java @@ -4,13 +4,6 @@ import java.util.Map; import java.util.UUID; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; -import com.fasterxml.jackson.annotation.JsonValue; -import com.fasterxml.jackson.core.type.TypeReference; import io.a2a.util.Assert; import static io.a2a.spec.Message.MESSAGE; @@ -18,13 +11,8 @@ /** * Represents a single message in the conversation between a user and an agent. */ -@JsonTypeName(MESSAGE) -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class Message implements EventKind, StreamingEventKind { - public static final TypeReference TYPE_REFERENCE = new TypeReference<>() {}; - public static final String MESSAGE = "message"; private final Role role; private final List> parts; @@ -41,12 +29,11 @@ public Message(Role role, List> parts, String messageId, String contextI this(role, parts, messageId, contextId, taskId, referenceTaskIds, metadata, extensions, MESSAGE); } - @JsonCreator - public Message(@JsonProperty("role") Role role, @JsonProperty("parts") List> parts, - @JsonProperty("messageId") String messageId, @JsonProperty("contextId") String contextId, - @JsonProperty("taskId") String taskId, @JsonProperty("referenceTaskIds") List referenceTaskIds, - @JsonProperty("metadata") Map metadata, @JsonProperty("extensions") List extensions, - @JsonProperty("kind") String kind) { + public Message(Role role, List> parts, + String messageId, String contextId, + String taskId, List referenceTaskIds, + Map metadata, List extensions, + String kind) { Assert.checkNotNullParam("kind", kind); Assert.checkNotNullParam("parts", parts); if (parts.isEmpty()) { @@ -123,7 +110,11 @@ public enum Role { this.role = role; } - @JsonValue + /** + * Returns the string representation of the role for JSON serialization. + * + * @return the role as a string ("user" or "agent") + */ public String asString() { return this.role; } diff --git a/spec/src/main/java/io/a2a/spec/MessageSendConfiguration.java b/spec/src/main/java/io/a2a/spec/MessageSendConfiguration.java index cbe72cfb1..d44ce494f 100644 --- a/spec/src/main/java/io/a2a/spec/MessageSendConfiguration.java +++ b/spec/src/main/java/io/a2a/spec/MessageSendConfiguration.java @@ -2,16 +2,12 @@ import java.util.List; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; /** * Defines configuration options for a `message/send` or `message/stream` request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record MessageSendConfiguration(List acceptedOutputModes, Integer historyLength, PushNotificationConfig pushNotificationConfig, Boolean blocking) { diff --git a/spec/src/main/java/io/a2a/spec/MessageSendParams.java b/spec/src/main/java/io/a2a/spec/MessageSendParams.java index 7cba6f1ae..5914ef462 100644 --- a/spec/src/main/java/io/a2a/spec/MessageSendParams.java +++ b/spec/src/main/java/io/a2a/spec/MessageSendParams.java @@ -2,16 +2,12 @@ import java.util.Map; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; import io.a2a.util.Assert; /** * Defines the parameters for a request to send a message to an agent. This can be used * to create a new task, continue an existing one, or restart a task. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record MessageSendParams(Message message, MessageSendConfiguration configuration, Map metadata) { diff --git a/spec/src/main/java/io/a2a/spec/MethodNotFoundError.java b/spec/src/main/java/io/a2a/spec/MethodNotFoundError.java index 5a46b336f..e8af3590a 100644 --- a/spec/src/main/java/io/a2a/spec/MethodNotFoundError.java +++ b/spec/src/main/java/io/a2a/spec/MethodNotFoundError.java @@ -1,33 +1,20 @@ package io.a2a.spec; +import static io.a2a.spec.A2AErrorCodes.METHOD_NOT_FOUND_ERROR_CODE; import static io.a2a.util.Utils.defaultIfNull; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - /** * An error indicating that the requested method does not exist or is not available. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public class MethodNotFoundError extends JSONRPCError { - public final static Integer DEFAULT_CODE = -32601; + public final static Integer DEFAULT_CODE = METHOD_NOT_FOUND_ERROR_CODE; - @JsonCreator - public MethodNotFoundError( - @JsonProperty("code") Integer code, - @JsonProperty("message") String message, - @JsonProperty("data") Object data) { - super( - defaultIfNull(code, DEFAULT_CODE), - defaultIfNull(message, "Method not found"), - data); + public MethodNotFoundError(Integer code, String message, Object data) { + super(defaultIfNull(code, METHOD_NOT_FOUND_ERROR_CODE), defaultIfNull(message, "Method not found"), data); } public MethodNotFoundError() { - this(DEFAULT_CODE, null, null); + this(METHOD_NOT_FOUND_ERROR_CODE, null, null); } } diff --git a/spec/src/main/java/io/a2a/spec/MutualTLSSecurityScheme.java b/spec/src/main/java/io/a2a/spec/MutualTLSSecurityScheme.java index 37f5bd755..e0b6fb8e0 100644 --- a/spec/src/main/java/io/a2a/spec/MutualTLSSecurityScheme.java +++ b/spec/src/main/java/io/a2a/spec/MutualTLSSecurityScheme.java @@ -1,10 +1,5 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; import io.a2a.util.Assert; import static io.a2a.spec.MutualTLSSecurityScheme.MUTUAL_TLS; @@ -12,9 +7,6 @@ /** * Defines a security scheme using mTLS authentication. */ -@JsonTypeName(MUTUAL_TLS) -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class MutualTLSSecurityScheme implements SecurityScheme { public static final String MUTUAL_TLS = "mutualTLS"; @@ -29,9 +21,7 @@ public MutualTLSSecurityScheme() { this(null, MUTUAL_TLS); } - @JsonCreator - public MutualTLSSecurityScheme(@JsonProperty("description") String description, - @JsonProperty("type") String type) { + public MutualTLSSecurityScheme(String description, String type) { Assert.checkNotNullParam("type", type); if (!type.equals(MUTUAL_TLS)) { throw new IllegalArgumentException("Invalid type for MutualTLSSecurityScheme"); diff --git a/spec/src/main/java/io/a2a/spec/NonStreamingJSONRPCRequest.java b/spec/src/main/java/io/a2a/spec/NonStreamingJSONRPCRequest.java index f3ac7d2cb..e969ce08e 100644 --- a/spec/src/main/java/io/a2a/spec/NonStreamingJSONRPCRequest.java +++ b/spec/src/main/java/io/a2a/spec/NonStreamingJSONRPCRequest.java @@ -1,15 +1,8 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; - /** * Represents a non-streaming JSON-RPC request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) -@JsonDeserialize(using = NonStreamingJSONRPCRequestDeserializer.class) public abstract sealed class NonStreamingJSONRPCRequest extends JSONRPCRequest permits GetTaskRequest, CancelTaskRequest, SetTaskPushNotificationConfigRequest, GetTaskPushNotificationConfigRequest, SendMessageRequest, DeleteTaskPushNotificationConfigRequest, ListTaskPushNotificationConfigRequest, diff --git a/spec/src/main/java/io/a2a/spec/NonStreamingJSONRPCRequestDeserializer.java b/spec/src/main/java/io/a2a/spec/NonStreamingJSONRPCRequestDeserializer.java deleted file mode 100644 index 37366b9b7..000000000 --- a/spec/src/main/java/io/a2a/spec/NonStreamingJSONRPCRequestDeserializer.java +++ /dev/null @@ -1,58 +0,0 @@ -package io.a2a.spec; - -import java.io.IOException; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonNode; - -public class NonStreamingJSONRPCRequestDeserializer extends JSONRPCRequestDeserializerBase> { - - public NonStreamingJSONRPCRequestDeserializer() { - this(null); - } - - public NonStreamingJSONRPCRequestDeserializer(Class vc) { - super(vc); - } - - @Override - public NonStreamingJSONRPCRequest deserialize(JsonParser jsonParser, DeserializationContext context) - throws IOException, JsonProcessingException { - JsonNode treeNode = jsonParser.getCodec().readTree(jsonParser); - String jsonrpc = getAndValidateJsonrpc(treeNode, jsonParser); - String method = getAndValidateMethod(treeNode, jsonParser); - Object id = getAndValidateId(treeNode, jsonParser); - JsonNode paramsNode = treeNode.get("params"); - - switch (method) { - case GetTaskRequest.METHOD: - return new GetTaskRequest(jsonrpc, id, method, - getAndValidateParams(paramsNode, jsonParser, treeNode, TaskQueryParams.class)); - case CancelTaskRequest.METHOD: - return new CancelTaskRequest(jsonrpc, id, method, - getAndValidateParams(paramsNode, jsonParser, treeNode, TaskIdParams.class)); - case SetTaskPushNotificationConfigRequest.METHOD: - return new SetTaskPushNotificationConfigRequest(jsonrpc, id, method, - getAndValidateParams(paramsNode, jsonParser, treeNode, TaskPushNotificationConfig.class)); - case GetTaskPushNotificationConfigRequest.METHOD: - return new GetTaskPushNotificationConfigRequest(jsonrpc, id, method, - getAndValidateParams(paramsNode, jsonParser, treeNode, GetTaskPushNotificationConfigParams.class)); - case SendMessageRequest.METHOD: - return new SendMessageRequest(jsonrpc, id, method, - getAndValidateParams(paramsNode, jsonParser, treeNode, MessageSendParams.class)); - case ListTaskPushNotificationConfigRequest.METHOD: - return new ListTaskPushNotificationConfigRequest(jsonrpc, id, method, - getAndValidateParams(paramsNode, jsonParser, treeNode, ListTaskPushNotificationConfigParams.class)); - case DeleteTaskPushNotificationConfigRequest.METHOD: - return new DeleteTaskPushNotificationConfigRequest(jsonrpc, id, method, - getAndValidateParams(paramsNode, jsonParser, treeNode, DeleteTaskPushNotificationConfigParams.class)); - case GetAuthenticatedExtendedCardRequest.METHOD: - return new GetAuthenticatedExtendedCardRequest(jsonrpc, id, method, - getAndValidateParams(paramsNode, jsonParser, treeNode, Void.class)); - default: - throw new MethodNotFoundJsonMappingException("Invalid method", getIdIfPossible(treeNode, jsonParser)); - } - } -} diff --git a/spec/src/main/java/io/a2a/spec/OAuth2SecurityScheme.java b/spec/src/main/java/io/a2a/spec/OAuth2SecurityScheme.java index 9094eee7c..cd456864a 100644 --- a/spec/src/main/java/io/a2a/spec/OAuth2SecurityScheme.java +++ b/spec/src/main/java/io/a2a/spec/OAuth2SecurityScheme.java @@ -1,11 +1,5 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -import com.fasterxml.jackson.annotation.JsonTypeName; import io.a2a.util.Assert; import static io.a2a.spec.OAuth2SecurityScheme.OAUTH2; @@ -13,9 +7,6 @@ /** * Defines a security scheme using OAuth 2.0. */ -@JsonTypeName(OAUTH2) -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class OAuth2SecurityScheme implements SecurityScheme { public static final String OAUTH2 = "oauth2"; @@ -28,9 +19,7 @@ public OAuth2SecurityScheme(OAuthFlows flows, String description, String oauth2M this(flows, description, oauth2MetadataUrl, OAUTH2); } - @JsonCreator - public OAuth2SecurityScheme(@JsonProperty("flows") OAuthFlows flows, @JsonProperty("description") String description, - @JsonProperty("oauth2MetadataUrl") String oauth2MetadataUrl, @JsonProperty("type") String type) { + public OAuth2SecurityScheme(OAuthFlows flows, String description, String oauth2MetadataUrl, String type) { Assert.checkNotNullParam("flows", flows); Assert.checkNotNullParam("type", type); if (!type.equals(OAUTH2)) { diff --git a/spec/src/main/java/io/a2a/spec/OAuthFlows.java b/spec/src/main/java/io/a2a/spec/OAuthFlows.java index 8c5015893..849f84d41 100644 --- a/spec/src/main/java/io/a2a/spec/OAuthFlows.java +++ b/spec/src/main/java/io/a2a/spec/OAuthFlows.java @@ -1,13 +1,8 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; - /** * Defines the configuration for the supported OAuth 2.0 flows. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record OAuthFlows(AuthorizationCodeOAuthFlow authorizationCode, ClientCredentialsOAuthFlow clientCredentials, ImplicitOAuthFlow implicit, PasswordOAuthFlow password) { diff --git a/spec/src/main/java/io/a2a/spec/OpenIdConnectSecurityScheme.java b/spec/src/main/java/io/a2a/spec/OpenIdConnectSecurityScheme.java index 01726d2ea..949fd2a68 100644 --- a/spec/src/main/java/io/a2a/spec/OpenIdConnectSecurityScheme.java +++ b/spec/src/main/java/io/a2a/spec/OpenIdConnectSecurityScheme.java @@ -1,10 +1,5 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; import io.a2a.util.Assert; import static io.a2a.spec.OpenIdConnectSecurityScheme.OPENID_CONNECT; @@ -12,9 +7,6 @@ /** * Defines a security scheme using OpenID Connect. */ -@JsonTypeName(OPENID_CONNECT) -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class OpenIdConnectSecurityScheme implements SecurityScheme { public static final String OPENID_CONNECT = "openIdConnect"; @@ -26,9 +18,7 @@ public OpenIdConnectSecurityScheme(String openIdConnectUrl, String description) this(openIdConnectUrl, description, OPENID_CONNECT); } - @JsonCreator - public OpenIdConnectSecurityScheme(@JsonProperty("openIdConnectUrl") String openIdConnectUrl, - @JsonProperty("description") String description, @JsonProperty("type") String type) { + public OpenIdConnectSecurityScheme(String openIdConnectUrl, String description, String type) { Assert.checkNotNullParam("type", type); Assert.checkNotNullParam("openIdConnectUrl", openIdConnectUrl); if (!type.equals(OPENID_CONNECT)) { diff --git a/spec/src/main/java/io/a2a/spec/Part.java b/spec/src/main/java/io/a2a/spec/Part.java index 2bdfb69d1..e55577622 100644 --- a/spec/src/main/java/io/a2a/spec/Part.java +++ b/spec/src/main/java/io/a2a/spec/Part.java @@ -2,25 +2,10 @@ import java.util.Map; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.annotation.JsonValue; - /** * A fundamental unit with a Message or Artifact. * @param the type of unit */ -@JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "kind", - visible = true -) -@JsonSubTypes({ - @JsonSubTypes.Type(value = TextPart.class, name = TextPart.TEXT), - @JsonSubTypes.Type(value = FilePart.class, name = FilePart.FILE), - @JsonSubTypes.Type(value = DataPart.class, name = DataPart.DATA) -}) public abstract class Part { public enum Kind { TEXT("text"), @@ -33,7 +18,11 @@ public enum Kind { this.kind = kind; } - @JsonValue + /** + * Returns the string representation of the kind for JSON serialization. + * + * @return the kind as a string + */ public String asString() { return this.kind; } diff --git a/spec/src/main/java/io/a2a/spec/PasswordOAuthFlow.java b/spec/src/main/java/io/a2a/spec/PasswordOAuthFlow.java index 424a4817f..e5de924cb 100644 --- a/spec/src/main/java/io/a2a/spec/PasswordOAuthFlow.java +++ b/spec/src/main/java/io/a2a/spec/PasswordOAuthFlow.java @@ -2,16 +2,11 @@ import java.util.Map; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; - import io.a2a.util.Assert; /** * Defines configuration details for the OAuth 2.0 Resource Owner Password flow. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record PasswordOAuthFlow(String refreshUrl, Map scopes, String tokenUrl) { public PasswordOAuthFlow { diff --git a/spec/src/main/java/io/a2a/spec/PushNotificationAuthenticationInfo.java b/spec/src/main/java/io/a2a/spec/PushNotificationAuthenticationInfo.java index 1ef29ccd4..6263ac990 100644 --- a/spec/src/main/java/io/a2a/spec/PushNotificationAuthenticationInfo.java +++ b/spec/src/main/java/io/a2a/spec/PushNotificationAuthenticationInfo.java @@ -1,16 +1,11 @@ package io.a2a.spec; import java.util.List; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; import io.a2a.util.Assert; /** * Defines authentication details for a push notification endpoint. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record PushNotificationAuthenticationInfo(List schemes, String credentials) { public PushNotificationAuthenticationInfo { diff --git a/spec/src/main/java/io/a2a/spec/PushNotificationConfig.java b/spec/src/main/java/io/a2a/spec/PushNotificationConfig.java index fcffa2c7f..b5a9e1131 100644 --- a/spec/src/main/java/io/a2a/spec/PushNotificationConfig.java +++ b/spec/src/main/java/io/a2a/spec/PushNotificationConfig.java @@ -1,17 +1,11 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.core.type.TypeReference; import io.a2a.util.Assert; /** * Defines the configuration for setting up push notifications for task updates. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record PushNotificationConfig(String url, String token, PushNotificationAuthenticationInfo authentication, String id) { - public static final TypeReference TYPE_REFERENCE = new TypeReference<>() {}; public PushNotificationConfig { Assert.checkNotNullParam("url", url); diff --git a/spec/src/main/java/io/a2a/spec/PushNotificationNotSupportedError.java b/spec/src/main/java/io/a2a/spec/PushNotificationNotSupportedError.java index c618e2b78..7397acd74 100644 --- a/spec/src/main/java/io/a2a/spec/PushNotificationNotSupportedError.java +++ b/spec/src/main/java/io/a2a/spec/PushNotificationNotSupportedError.java @@ -2,16 +2,9 @@ import static io.a2a.util.Utils.defaultIfNull; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - /** * An A2A-specific error indicating that the agent does not support push notifications. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public class PushNotificationNotSupportedError extends JSONRPCError { public final static Integer DEFAULT_CODE = -32003; @@ -20,11 +13,10 @@ public PushNotificationNotSupportedError() { this(null, null, null); } - @JsonCreator public PushNotificationNotSupportedError( - @JsonProperty("code") Integer code, - @JsonProperty("message") String message, - @JsonProperty("data") Object data) { + Integer code, + String message, + Object data) { super( defaultIfNull(code, DEFAULT_CODE), defaultIfNull(message, "Push Notification is not supported"), diff --git a/spec/src/main/java/io/a2a/spec/SecurityScheme.java b/spec/src/main/java/io/a2a/spec/SecurityScheme.java index b3cc104e3..a39dfd314 100644 --- a/spec/src/main/java/io/a2a/spec/SecurityScheme.java +++ b/spec/src/main/java/io/a2a/spec/SecurityScheme.java @@ -2,22 +2,6 @@ import static io.a2a.spec.APIKeySecurityScheme.API_KEY; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; - -@JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true -) -@JsonSubTypes({ - @JsonSubTypes.Type(value = APIKeySecurityScheme.class, name = API_KEY), - @JsonSubTypes.Type(value = HTTPAuthSecurityScheme.class, name = HTTPAuthSecurityScheme.HTTP), - @JsonSubTypes.Type(value = OAuth2SecurityScheme.class, name = OAuth2SecurityScheme.OAUTH2), - @JsonSubTypes.Type(value = OpenIdConnectSecurityScheme.class, name = OpenIdConnectSecurityScheme.OPENID_CONNECT), - @JsonSubTypes.Type(value = MutualTLSSecurityScheme.class, name = MutualTLSSecurityScheme.MUTUAL_TLS) -}) /** * Defines a security scheme that can be used to secure an agent's endpoints. * This is a discriminated union type based on the OpenAPI 3.0 Security Scheme Object. diff --git a/spec/src/main/java/io/a2a/spec/SendMessageRequest.java b/spec/src/main/java/io/a2a/spec/SendMessageRequest.java index d31f364e4..a58ce0890 100644 --- a/spec/src/main/java/io/a2a/spec/SendMessageRequest.java +++ b/spec/src/main/java/io/a2a/spec/SendMessageRequest.java @@ -4,25 +4,28 @@ import java.util.UUID; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - import io.a2a.util.Assert; /** * Used to send a message request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class SendMessageRequest extends NonStreamingJSONRPCRequest { public static final String METHOD = "message/send"; - @JsonCreator - public SendMessageRequest(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("method") String method, @JsonProperty("params") MessageSendParams params) { + /** + * Constructs a SendMessageRequest with the specified JSON-RPC fields. + *

+ * This constructor is used for JSON deserialization and validates + * that the method name is exactly "SendMessage". + * + * @param jsonrpc the JSON-RPC version (must be "2.0") + * @param id the request correlation identifier (String, Integer, or null) + * @param method the method name (must be {@value #METHOD}) + * @param params the message send parameters (required) + * @throws IllegalArgumentException if validation fails + */ + public SendMessageRequest(String jsonrpc, Object id, String method, MessageSendParams params) { if (jsonrpc == null || jsonrpc.isEmpty()) { throw new IllegalArgumentException("JSON-RPC protocol version cannot be null or empty"); } diff --git a/spec/src/main/java/io/a2a/spec/SendMessageResponse.java b/spec/src/main/java/io/a2a/spec/SendMessageResponse.java index 901beba90..14d3d5197 100644 --- a/spec/src/main/java/io/a2a/spec/SendMessageResponse.java +++ b/spec/src/main/java/io/a2a/spec/SendMessageResponse.java @@ -1,23 +1,11 @@ package io.a2a.spec; -import static io.a2a.util.Utils.defaultIfNull; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import io.a2a.util.Assert; - /** * The response after receiving a send message request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class SendMessageResponse extends JSONRPCResponse { - @JsonCreator - public SendMessageResponse(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("result") EventKind result, @JsonProperty("error") JSONRPCError error) { + public SendMessageResponse(String jsonrpc, Object id, EventKind result, JSONRPCError error) { super(jsonrpc, id, result, error, EventKind.class); } diff --git a/spec/src/main/java/io/a2a/spec/SendStreamingMessageRequest.java b/spec/src/main/java/io/a2a/spec/SendStreamingMessageRequest.java index c4ebe8315..de3abf950 100644 --- a/spec/src/main/java/io/a2a/spec/SendStreamingMessageRequest.java +++ b/spec/src/main/java/io/a2a/spec/SendStreamingMessageRequest.java @@ -2,10 +2,6 @@ import static io.a2a.util.Utils.defaultIfNull; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; import io.a2a.util.Assert; import java.util.UUID; @@ -13,15 +9,11 @@ /** * Used to initiate a task with streaming. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class SendStreamingMessageRequest extends StreamingJSONRPCRequest { public static final String METHOD = "message/stream"; - @JsonCreator - public SendStreamingMessageRequest(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("method") String method, @JsonProperty("params") MessageSendParams params) { + public SendStreamingMessageRequest(String jsonrpc, Object id, String method, MessageSendParams params) { if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); } diff --git a/spec/src/main/java/io/a2a/spec/SendStreamingMessageResponse.java b/spec/src/main/java/io/a2a/spec/SendStreamingMessageResponse.java index 0ed6ca80d..d76ea9aef 100644 --- a/spec/src/main/java/io/a2a/spec/SendStreamingMessageResponse.java +++ b/spec/src/main/java/io/a2a/spec/SendStreamingMessageResponse.java @@ -1,21 +1,11 @@ package io.a2a.spec; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - /** * The response after receiving a request to initiate a task with streaming. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class SendStreamingMessageResponse extends JSONRPCResponse { - @JsonCreator - public SendStreamingMessageResponse(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("result") StreamingEventKind result, @JsonProperty("error") JSONRPCError error) { + public SendStreamingMessageResponse(String jsonrpc, Object id, StreamingEventKind result, JSONRPCError error) { super(jsonrpc, id, result, error, StreamingEventKind.class); } diff --git a/spec/src/main/java/io/a2a/spec/SetTaskPushNotificationConfigRequest.java b/spec/src/main/java/io/a2a/spec/SetTaskPushNotificationConfigRequest.java index 7d53083a1..3007112ff 100644 --- a/spec/src/main/java/io/a2a/spec/SetTaskPushNotificationConfigRequest.java +++ b/spec/src/main/java/io/a2a/spec/SetTaskPushNotificationConfigRequest.java @@ -4,25 +4,16 @@ import java.util.UUID; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - import io.a2a.util.Assert; /** * Used to set a task push notification request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class SetTaskPushNotificationConfigRequest extends NonStreamingJSONRPCRequest { public static final String METHOD = "tasks/pushNotificationConfig/set"; - @JsonCreator - public SetTaskPushNotificationConfigRequest(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("method") String method, @JsonProperty("params") TaskPushNotificationConfig params) { + public SetTaskPushNotificationConfigRequest(String jsonrpc, Object id, String method, TaskPushNotificationConfig params) { if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); } diff --git a/spec/src/main/java/io/a2a/spec/SetTaskPushNotificationConfigResponse.java b/spec/src/main/java/io/a2a/spec/SetTaskPushNotificationConfigResponse.java index c40f18f18..1b5bd2e52 100644 --- a/spec/src/main/java/io/a2a/spec/SetTaskPushNotificationConfigResponse.java +++ b/spec/src/main/java/io/a2a/spec/SetTaskPushNotificationConfigResponse.java @@ -1,21 +1,11 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - /** * The response after receiving a set task push notification request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class SetTaskPushNotificationConfigResponse extends JSONRPCResponse { - @JsonCreator - public SetTaskPushNotificationConfigResponse(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("result") TaskPushNotificationConfig result, - @JsonProperty("error") JSONRPCError error) { + public SetTaskPushNotificationConfigResponse(String jsonrpc, Object id, TaskPushNotificationConfig result, JSONRPCError error) { super(jsonrpc, id, result, error, TaskPushNotificationConfig.class); } diff --git a/spec/src/main/java/io/a2a/spec/StreamingEventKind.java b/spec/src/main/java/io/a2a/spec/StreamingEventKind.java index b8c16b00a..73c141953 100644 --- a/spec/src/main/java/io/a2a/spec/StreamingEventKind.java +++ b/spec/src/main/java/io/a2a/spec/StreamingEventKind.java @@ -1,25 +1,34 @@ package io.a2a.spec; -import static io.a2a.spec.Message.MESSAGE; -import static io.a2a.spec.Task.TASK; -import static io.a2a.spec.TaskArtifactUpdateEvent.ARTIFACT_UPDATE; -import static io.a2a.spec.TaskStatusUpdateEvent.STATUS_UPDATE; - -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; - -@JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "kind", - visible = true -) -@JsonSubTypes({ - @JsonSubTypes.Type(value = Task.class, name = TASK), - @JsonSubTypes.Type(value = Message.class, name = MESSAGE), - @JsonSubTypes.Type(value = TaskStatusUpdateEvent.class, name = STATUS_UPDATE), - @JsonSubTypes.Type(value = TaskArtifactUpdateEvent.class, name = ARTIFACT_UPDATE) -}) +/** + * Sealed interface for events that can be emitted during streaming A2A Protocol operations. + *

+ * StreamingEventKind represents events suitable for asynchronous, progressive updates during + * agent task execution. Streaming allows agents to provide incremental feedback as work progresses, + * rather than waiting until task completion. + *

+ * Streaming events use polymorphic JSON serialization with the "kind" discriminator to determine + * the concrete type during deserialization. + *

+ * Permitted implementations: + *

    + *
  • {@link Task} - Complete task state (typically the final event in a stream)
  • + *
  • {@link Message} - Full message (complete message in the stream)
  • + *
  • {@link TaskStatusUpdateEvent} - Incremental status updates (e.g., SUBMITTED → WORKING → COMPLETED)
  • + *
  • {@link TaskArtifactUpdateEvent} - Incremental artifact updates (partial results, chunks)
  • + *
+ *

+ * Streaming events enable patterns like: + *

    + *
  • Progressive text generation (chunks of response as generated)
  • + *
  • Status notifications (task state transitions)
  • + *
  • Partial results (early artifacts before task completion)
  • + *
+ * + * @see Event + * @see EventKind + * @see UpdateEvent + */ public sealed interface StreamingEventKind extends Event permits Task, Message, TaskStatusUpdateEvent, TaskArtifactUpdateEvent { String getKind(); diff --git a/spec/src/main/java/io/a2a/spec/StreamingJSONRPCRequest.java b/spec/src/main/java/io/a2a/spec/StreamingJSONRPCRequest.java index 7642c41d2..9cbb5e4c6 100644 --- a/spec/src/main/java/io/a2a/spec/StreamingJSONRPCRequest.java +++ b/spec/src/main/java/io/a2a/spec/StreamingJSONRPCRequest.java @@ -1,15 +1,9 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; - /** * Represents a streaming JSON-RPC request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) -@JsonDeserialize(using = StreamingJSONRPCRequestDeserializer.class) + public abstract sealed class StreamingJSONRPCRequest extends JSONRPCRequest permits TaskResubscriptionRequest, SendStreamingMessageRequest { diff --git a/spec/src/main/java/io/a2a/spec/StreamingJSONRPCRequestDeserializer.java b/spec/src/main/java/io/a2a/spec/StreamingJSONRPCRequestDeserializer.java deleted file mode 100644 index b66533620..000000000 --- a/spec/src/main/java/io/a2a/spec/StreamingJSONRPCRequestDeserializer.java +++ /dev/null @@ -1,41 +0,0 @@ -package io.a2a.spec; - -import java.io.IOException; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonNode; - -public class StreamingJSONRPCRequestDeserializer extends JSONRPCRequestDeserializerBase> { - - public StreamingJSONRPCRequestDeserializer() { - this(null); - } - - public StreamingJSONRPCRequestDeserializer(Class vc) { - super(vc); - } - - @Override - public StreamingJSONRPCRequest deserialize(JsonParser jsonParser, DeserializationContext context) - throws IOException, JsonProcessingException { - JsonNode treeNode = jsonParser.getCodec().readTree(jsonParser); - String jsonrpc = getAndValidateJsonrpc(treeNode, jsonParser); - String method = getAndValidateMethod(treeNode, jsonParser); - Object id = getAndValidateId(treeNode, jsonParser); - JsonNode paramsNode = treeNode.get("params"); - - switch (method) { - case TaskResubscriptionRequest.METHOD -> { - return new TaskResubscriptionRequest(jsonrpc, id, method, - getAndValidateParams(paramsNode, jsonParser, treeNode, TaskIdParams.class)); - } - case SendStreamingMessageRequest.METHOD -> { - return new SendStreamingMessageRequest(jsonrpc, id, method, - getAndValidateParams(paramsNode, jsonParser, treeNode, MessageSendParams.class)); - } - default -> throw new MethodNotFoundJsonMappingException("Invalid method", getIdIfPossible(treeNode, jsonParser)); - } - } -} diff --git a/spec/src/main/java/io/a2a/spec/Task.java b/spec/src/main/java/io/a2a/spec/Task.java index 873f23431..b3b886e5b 100644 --- a/spec/src/main/java/io/a2a/spec/Task.java +++ b/spec/src/main/java/io/a2a/spec/Task.java @@ -3,12 +3,6 @@ import java.util.List; import java.util.Map; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; -import com.fasterxml.jackson.core.type.TypeReference; import io.a2a.util.Assert; import static io.a2a.spec.Task.TASK; @@ -16,13 +10,8 @@ /** * Represents a single, stateful operation or conversation between a client and an agent. */ -@JsonTypeName(TASK) -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class Task implements EventKind, StreamingEventKind { - public static final TypeReference TYPE_REFERENCE = new TypeReference<>() {}; - public static final String TASK = "task"; private final String id; private final String contextId; @@ -37,10 +26,9 @@ public Task(String id, String contextId, TaskStatus status, List artif this(id, contextId, status, artifacts, history, metadata, TASK); } - @JsonCreator - public Task(@JsonProperty("id") String id, @JsonProperty("contextId") String contextId, @JsonProperty("status") TaskStatus status, - @JsonProperty("artifacts") List artifacts, @JsonProperty("history") List history, - @JsonProperty("metadata") Map metadata, @JsonProperty("kind") String kind) { + public Task(String id, String contextId, TaskStatus status, + List artifacts, List history, + Map metadata, String kind) { Assert.checkNotNullParam("id", id); Assert.checkNotNullParam("contextId", contextId); Assert.checkNotNullParam("status", status); diff --git a/spec/src/main/java/io/a2a/spec/TaskArtifactUpdateEvent.java b/spec/src/main/java/io/a2a/spec/TaskArtifactUpdateEvent.java index 1e1cad947..426a3da92 100644 --- a/spec/src/main/java/io/a2a/spec/TaskArtifactUpdateEvent.java +++ b/spec/src/main/java/io/a2a/spec/TaskArtifactUpdateEvent.java @@ -2,11 +2,6 @@ import java.util.Map; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; import io.a2a.util.Assert; import static io.a2a.spec.TaskArtifactUpdateEvent.ARTIFACT_UPDATE; @@ -15,9 +10,6 @@ * An event sent by the agent to notify the client that an artifact has been * generated or updated. This is typically used in streaming models. */ -@JsonTypeName(ARTIFACT_UPDATE) -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class TaskArtifactUpdateEvent implements EventKind, StreamingEventKind, UpdateEvent { public static final String ARTIFACT_UPDATE = "artifact-update"; @@ -33,13 +25,7 @@ public TaskArtifactUpdateEvent(String taskId, Artifact artifact, String contextI this(taskId, artifact, contextId, append, lastChunk, metadata, ARTIFACT_UPDATE); } - @JsonCreator - public TaskArtifactUpdateEvent(@JsonProperty("taskId") String taskId, @JsonProperty("artifact") Artifact artifact, - @JsonProperty("contextId") String contextId, - @JsonProperty("append") Boolean append, - @JsonProperty("lastChunk") Boolean lastChunk, - @JsonProperty("metadata") Map metadata, - @JsonProperty("kind") String kind) { + public TaskArtifactUpdateEvent(String taskId, Artifact artifact, String contextId, Boolean append, Boolean lastChunk, Map metadata, String kind) { Assert.checkNotNullParam("taskId", taskId); Assert.checkNotNullParam("artifact", artifact); Assert.checkNotNullParam("contextId", contextId); diff --git a/spec/src/main/java/io/a2a/spec/TaskIdParams.java b/spec/src/main/java/io/a2a/spec/TaskIdParams.java index 237fe727a..096c9a8a0 100644 --- a/spec/src/main/java/io/a2a/spec/TaskIdParams.java +++ b/spec/src/main/java/io/a2a/spec/TaskIdParams.java @@ -2,15 +2,11 @@ import java.util.Map; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; import io.a2a.util.Assert; /** * Defines parameters containing a task ID, used for simple task operations. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record TaskIdParams(String id, Map metadata) { public TaskIdParams { diff --git a/spec/src/main/java/io/a2a/spec/TaskNotCancelableError.java b/spec/src/main/java/io/a2a/spec/TaskNotCancelableError.java index 50d32c385..d9b77bca0 100644 --- a/spec/src/main/java/io/a2a/spec/TaskNotCancelableError.java +++ b/spec/src/main/java/io/a2a/spec/TaskNotCancelableError.java @@ -2,16 +2,9 @@ import static io.a2a.util.Utils.defaultIfNull; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - /** * An A2A-specific error indicating that the task is in a state where it cannot be canceled. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public class TaskNotCancelableError extends JSONRPCError { public final static Integer DEFAULT_CODE = -32002; @@ -20,18 +13,17 @@ public TaskNotCancelableError() { this(null, null, null); } - @JsonCreator public TaskNotCancelableError( - @JsonProperty("code") Integer code, - @JsonProperty("message") String message, - @JsonProperty("data") Object data) { + Integer code, + String message, + Object data) { super( defaultIfNull(code, DEFAULT_CODE), defaultIfNull(message, "Task cannot be canceled"), data); } - public TaskNotCancelableError(@JsonProperty("message") String message) { + public TaskNotCancelableError(String message) { this(null, message, null); } diff --git a/spec/src/main/java/io/a2a/spec/TaskNotFoundError.java b/spec/src/main/java/io/a2a/spec/TaskNotFoundError.java index c21998d2d..40a1528f7 100644 --- a/spec/src/main/java/io/a2a/spec/TaskNotFoundError.java +++ b/spec/src/main/java/io/a2a/spec/TaskNotFoundError.java @@ -1,33 +1,21 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - import static io.a2a.util.Utils.defaultIfNull; /** * An A2A-specific error indicating that the requested task ID was not found. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public class TaskNotFoundError extends JSONRPCError { - public final static Integer DEFAULT_CODE = -32001; + public final static Integer DEFAULT_CODE = A2AErrorCodes.TASK_NOT_FOUND_ERROR_CODE; public TaskNotFoundError() { this(null, null, null); } - @JsonCreator - - public TaskNotFoundError( - @JsonProperty("code") Integer code, - @JsonProperty("message") String message, - @JsonProperty("data") Object data) { + public TaskNotFoundError(Integer code, String message, Object data) { super( - defaultIfNull(code, DEFAULT_CODE), + defaultIfNull(code, A2AErrorCodes.TASK_NOT_FOUND_ERROR_CODE), defaultIfNull(message, "Task not found"), data); } diff --git a/spec/src/main/java/io/a2a/spec/TaskPushNotificationConfig.java b/spec/src/main/java/io/a2a/spec/TaskPushNotificationConfig.java index 4cda7a8f3..23a7fc0c4 100644 --- a/spec/src/main/java/io/a2a/spec/TaskPushNotificationConfig.java +++ b/spec/src/main/java/io/a2a/spec/TaskPushNotificationConfig.java @@ -1,14 +1,10 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; import io.a2a.util.Assert; /** * A container associating a push notification configuration with a specific task. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record TaskPushNotificationConfig(String taskId, PushNotificationConfig pushNotificationConfig) { public TaskPushNotificationConfig { diff --git a/spec/src/main/java/io/a2a/spec/TaskQueryParams.java b/spec/src/main/java/io/a2a/spec/TaskQueryParams.java index 92c2453c5..f119bd14b 100644 --- a/spec/src/main/java/io/a2a/spec/TaskQueryParams.java +++ b/spec/src/main/java/io/a2a/spec/TaskQueryParams.java @@ -1,8 +1,5 @@ package io.a2a.spec; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; import io.a2a.util.Assert; import java.util.Map; import org.jspecify.annotations.Nullable; @@ -14,9 +11,6 @@ * @param historyLength the maximum number of items of history for the task to include in the response * @param metadata additional properties */ - -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record TaskQueryParams(String id, int historyLength, @Nullable Map metadata) { public TaskQueryParams { diff --git a/spec/src/main/java/io/a2a/spec/TaskResubscriptionRequest.java b/spec/src/main/java/io/a2a/spec/TaskResubscriptionRequest.java index de0c88c62..951615d53 100644 --- a/spec/src/main/java/io/a2a/spec/TaskResubscriptionRequest.java +++ b/spec/src/main/java/io/a2a/spec/TaskResubscriptionRequest.java @@ -2,10 +2,6 @@ import static io.a2a.util.Utils.defaultIfNull; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; import io.a2a.util.Assert; import java.util.UUID; @@ -13,15 +9,11 @@ /** * Used to resubscribe to a task. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class TaskResubscriptionRequest extends StreamingJSONRPCRequest { public static final String METHOD = "tasks/resubscribe"; - @JsonCreator - public TaskResubscriptionRequest(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("method") String method, @JsonProperty("params") TaskIdParams params) { + public TaskResubscriptionRequest(String jsonrpc, Object id, String method, TaskIdParams params) { if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); } diff --git a/spec/src/main/java/io/a2a/spec/TaskState.java b/spec/src/main/java/io/a2a/spec/TaskState.java index 655d20919..413044810 100644 --- a/spec/src/main/java/io/a2a/spec/TaskState.java +++ b/spec/src/main/java/io/a2a/spec/TaskState.java @@ -1,8 +1,5 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; - /** * Defines the lifecycle states of a Task. */ @@ -29,7 +26,14 @@ public enum TaskState { this.isFinal = isFinal; } - @JsonValue + /** + * Returns the string representation of this task state for JSON serialization. + *

+ * This method is used to serialize TaskState values to their + * wire format (e.g., "working", "completed"). + * + * @return the string representation of this state + */ public String asString() { return state; } @@ -38,7 +42,16 @@ public boolean isFinal(){ return isFinal; } - @JsonCreator + /** + * Deserializes a string value into a TaskState enum constant. + *

+ * This method is used to deserialize TaskState values from their + * wire format during JSON parsing. + * + * @param state the string representation of the state + * @return the corresponding TaskState enum constant + * @throws IllegalArgumentException if the state string is not recognized + */ public static TaskState fromString(String state) { return switch (state) { case "submitted" -> SUBMITTED; diff --git a/spec/src/main/java/io/a2a/spec/TaskStatus.java b/spec/src/main/java/io/a2a/spec/TaskStatus.java index 0b9bed654..a94d54580 100644 --- a/spec/src/main/java/io/a2a/spec/TaskStatus.java +++ b/spec/src/main/java/io/a2a/spec/TaskStatus.java @@ -3,19 +3,13 @@ import java.time.OffsetDateTime; import java.time.ZoneOffset; -import com.fasterxml.jackson.annotation.JsonFormat; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; - import io.a2a.util.Assert; /** * Represents the status of a task at a specific point in time. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record TaskStatus(TaskState state, Message message, - @JsonFormat(shape = JsonFormat.Shape.STRING) OffsetDateTime timestamp) { + OffsetDateTime timestamp) { public TaskStatus { Assert.checkNotNullParam("state", state); diff --git a/spec/src/main/java/io/a2a/spec/TaskStatusUpdateEvent.java b/spec/src/main/java/io/a2a/spec/TaskStatusUpdateEvent.java index 25e2cd170..409510c54 100644 --- a/spec/src/main/java/io/a2a/spec/TaskStatusUpdateEvent.java +++ b/spec/src/main/java/io/a2a/spec/TaskStatusUpdateEvent.java @@ -1,12 +1,8 @@ package io.a2a.spec; +import com.google.gson.annotations.SerializedName; import java.util.Map; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; import io.a2a.util.Assert; import static io.a2a.spec.TaskStatusUpdateEvent.STATUS_UPDATE; @@ -15,15 +11,13 @@ * An event sent by the agent to notify the client of a change in a task's status. * This is typically used in streaming or subscription models. */ -@JsonTypeName(STATUS_UPDATE) -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class TaskStatusUpdateEvent implements EventKind, StreamingEventKind, UpdateEvent { public static final String STATUS_UPDATE = "status-update"; private final String taskId; private final TaskStatus status; private final String contextId; + @SerializedName("final") private final boolean isFinal; private final Map metadata; private final String kind; @@ -34,10 +28,7 @@ public TaskStatusUpdateEvent(String taskId, TaskStatus status, String contextId, this(taskId, status, contextId, isFinal, metadata, STATUS_UPDATE); } - @JsonCreator - public TaskStatusUpdateEvent(@JsonProperty("taskId") String taskId, @JsonProperty("status") TaskStatus status, - @JsonProperty("contextId") String contextId, @JsonProperty("final") boolean isFinal, - @JsonProperty("metadata") Map metadata, @JsonProperty("kind") String kind) { + public TaskStatusUpdateEvent(String taskId, TaskStatus status, String contextId, boolean isFinal, Map metadata, String kind) { Assert.checkNotNullParam("taskId", taskId); Assert.checkNotNullParam("status", status); Assert.checkNotNullParam("contextId", contextId); @@ -65,7 +56,6 @@ public String getContextId() { return contextId; } - @JsonProperty("final") public boolean isFinal() { return isFinal; } diff --git a/spec/src/main/java/io/a2a/spec/TextPart.java b/spec/src/main/java/io/a2a/spec/TextPart.java index ade2047ee..0d44a3c6a 100644 --- a/spec/src/main/java/io/a2a/spec/TextPart.java +++ b/spec/src/main/java/io/a2a/spec/TextPart.java @@ -2,11 +2,6 @@ import java.util.Map; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; import io.a2a.util.Assert; import static io.a2a.spec.TextPart.TEXT; @@ -14,9 +9,6 @@ /** * Represents a text segment within a message or artifact. */ -@JsonTypeName(TEXT) -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public class TextPart extends Part { public static final String TEXT = "text"; @@ -28,8 +20,7 @@ public TextPart(String text) { this(text, null); } - @JsonCreator - public TextPart(@JsonProperty("text") String text, @JsonProperty("metadata") Map metadata) { + public TextPart(String text, Map metadata) { Assert.checkNotNullParam("text", text); this.text = text; this.metadata = metadata; diff --git a/spec/src/main/java/io/a2a/spec/TransportProtocol.java b/spec/src/main/java/io/a2a/spec/TransportProtocol.java index d6c2922d2..3b3446238 100644 --- a/spec/src/main/java/io/a2a/spec/TransportProtocol.java +++ b/spec/src/main/java/io/a2a/spec/TransportProtocol.java @@ -1,8 +1,5 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; - /** * Supported A2A transport protocols. */ @@ -17,12 +14,26 @@ public enum TransportProtocol { this.transport = transport; } - @JsonValue + /** + * Returns the string representation of this transport protocol. + *

+ * Used for JSON serialization. + * + * @return the transport protocol name as a string + */ public String asString() { return transport; } - @JsonCreator + /** + * Parses a string into a {@link TransportProtocol} enum constant. + *

+ * Used for JSON deserialization. + * + * @param transport the transport protocol string (e.g., "JSONRPC", "GRPC", "HTTP+JSON") + * @return the corresponding TransportProtocol enum constant + * @throws IllegalArgumentException if the transport string is not recognized + */ public static TransportProtocol fromString(String transport) { return switch (transport) { case "JSONRPC" -> JSONRPC; diff --git a/spec/src/main/java/io/a2a/spec/UnsupportedOperationError.java b/spec/src/main/java/io/a2a/spec/UnsupportedOperationError.java index 9fe055e9c..edcb381b0 100644 --- a/spec/src/main/java/io/a2a/spec/UnsupportedOperationError.java +++ b/spec/src/main/java/io/a2a/spec/UnsupportedOperationError.java @@ -2,27 +2,16 @@ import static io.a2a.util.Utils.defaultIfNull; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - /** * An A2A-specific error indicating that the requested operation is not supported by the agent. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public class UnsupportedOperationError extends JSONRPCError { - public final static Integer DEFAULT_CODE = -32004; + public final static Integer DEFAULT_CODE = A2AErrorCodes.UNSUPPORTED_OPERATION_ERROR_CODE; - @JsonCreator - public UnsupportedOperationError( - @JsonProperty("code") Integer code, - @JsonProperty("message") String message, - @JsonProperty("data") Object data) { + public UnsupportedOperationError(Integer code, String message, Object data) { super( - defaultIfNull(code, DEFAULT_CODE), + defaultIfNull(code, A2AErrorCodes.UNSUPPORTED_OPERATION_ERROR_CODE), defaultIfNull(message, "This operation is not supported"), data); } diff --git a/spec/src/main/java/io/a2a/util/Utils.java b/spec/src/main/java/io/a2a/util/Utils.java index 07143ab6a..f374f302e 100644 --- a/spec/src/main/java/io/a2a/util/Utils.java +++ b/spec/src/main/java/io/a2a/util/Utils.java @@ -2,29 +2,63 @@ import java.util.ArrayList; import java.util.List; -import java.util.logging.Logger; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.google.gson.Gson; +import io.a2a.json.JsonProcessingException; +import io.a2a.json.JsonUtil; import io.a2a.spec.Artifact; -import io.a2a.spec.Part; import io.a2a.spec.Task; import io.a2a.spec.TaskArtifactUpdateEvent; +import io.a2a.spec.Part; +import java.util.logging.Logger; + + +/** + * Utility class providing common helper methods for A2A Protocol operations. + *

+ * This class contains static utility methods for JSON serialization/deserialization, + * null-safe operations, artifact management, and other common tasks used throughout + * the A2A Java SDK. + *

+ * Key capabilities: + *

    + *
  • JSON processing with pre-configured {@link Gson}
  • + *
  • Null-safe value defaults via {@link #defaultIfNull(Object, Object)}
  • + *
  • Artifact streaming support via {@link #appendArtifactToTask(Task, TaskArtifactUpdateEvent, String)}
  • + *
  • Type-safe exception rethrowing via {@link #rethrow(Throwable)}
  • + *
+ * + * @see Gson for JSON processing + * @see TaskArtifactUpdateEvent for streaming artifact updates + */ public class Utils { - public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final Logger log = Logger.getLogger(Utils.class.getName()); - static { - // needed for date/time types - OBJECT_MAPPER.registerModule(new JavaTimeModule()); + + /** + * Deserializes JSON string into a typed object using Gson. + *

+ * This method uses the pre-configured {@link #OBJECT_MAPPER} to parse JSON. + * + * @param the target type + * @param data JSON string to deserialize + * @param typeRef class reference specifying the target type + * @return deserialized object of type T + * @throws JsonProcessingException if JSON parsing fails + */ + public static T unmarshalFrom(String data, Class typeRef) throws JsonProcessingException { + return JsonUtil.fromJson(data, typeRef); } - public static T unmarshalFrom(String data, TypeReference typeRef) throws JsonProcessingException { - return OBJECT_MAPPER.readValue(data, typeRef); + public static String toJsonString(Object data) { + try { + return JsonUtil.toJson(data); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to serialize to JSON", e); + } } public static T defaultIfNull(T value, T defaultValue) { @@ -38,6 +72,29 @@ public static void rethrow(Throwable t) throws T { throw (T) t; } + /** + * Appends or updates an artifact in a task based on a {@link TaskArtifactUpdateEvent}. + *

+ * This method handles streaming artifact updates, supporting both: + *

    + *
  • Adding new artifacts to the task
  • + *
  • Replacing existing artifacts (when {@code append=false})
  • + *
  • Appending parts to existing artifacts (when {@code append=true})
  • + *
+ *

+ * The {@code append} flag in the event determines the behavior: + *

    + *
  • {@code false} or {@code null}: Replace/add the entire artifact
  • + *
  • {@code true}: Append the new artifact's parts to an existing artifact with matching {@code artifactId}
  • + *
+ * + * @param task the current task to update + * @param event the artifact update event containing the new/updated artifact + * @param taskId the task ID (for logging purposes) + * @return a new Task instance with the updated artifacts list + * @see TaskArtifactUpdateEvent for streaming artifact updates + * @see Artifact for artifact structure + */ public static Task appendArtifactToTask(Task task, TaskArtifactUpdateEvent event, String taskId) { // Append artifacts List artifacts = task.getArtifacts() == null ? new ArrayList<>() : new ArrayList<>(task.getArtifacts()); @@ -94,12 +151,18 @@ public static Task appendArtifactToTask(Task task, TaskArtifactUpdateEvent event } - public static String toJsonString(Object o) { - try { - return OBJECT_MAPPER.writeValueAsString(o); - } catch (JsonProcessingException ex) { - throw new RuntimeException(ex); - } - } + /** + * Get the first defined URL in the supported interaces of the agent card. + * + * @param agentCard the agentcard where the interfaces are defined. + * @return the first defined URL in the supported interaces of the agent card. + * @throws A2AClientException + */ +// public static String getFavoriteInterface(AgentCard agentCard) throws A2AClientException { +// if (agentCard.supportedInterfaces() == null || agentCard.supportedInterfaces().isEmpty()) { +// throw new A2AClientException("No server interface available in the AgentCard"); +// } +// return agentCard.supportedInterfaces().get(0).url(); +// } } diff --git a/spec/src/test/java/io/a2a/spec/JSONRPCErrorSerializationTest.java b/spec/src/test/java/io/a2a/spec/JSONRPCErrorSerializationTest.java index 24251a879..9c83f0806 100644 --- a/spec/src/test/java/io/a2a/spec/JSONRPCErrorSerializationTest.java +++ b/spec/src/test/java/io/a2a/spec/JSONRPCErrorSerializationTest.java @@ -1,7 +1,5 @@ package io.a2a.spec; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import java.util.List; @@ -9,10 +7,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import io.a2a.json.JsonProcessingException; +import io.a2a.json.JsonUtil; + + public class JSONRPCErrorSerializationTest { @Test - public void shouldDeserializeToCorrectJSONRPCErrorSubclass() { - ObjectMapper objectMapper = new ObjectMapper(); + public void shouldDeserializeToCorrectJSONRPCErrorSubclass() throws JsonProcessingException { String jsonTemplate = """ {"code": %s, "message": "error", "data": "anything"} """; @@ -36,12 +37,7 @@ record ErrorCase(int code, Class clazz) {} for (ErrorCase errorCase : cases) { String json = jsonTemplate.formatted(errorCase.code()); - JSONRPCError error; - try { - error = objectMapper.readValue(json, JSONRPCError.class); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } + JSONRPCError error = JsonUtil.fromJson(json, JSONRPCError.class); assertInstanceOf(errorCase.clazz(), error); assertEquals("error", error.getMessage()); assertEquals("anything", error.getData().toString()); diff --git a/spec/src/test/java/io/a2a/spec/SubTypeSerializationTest.java b/spec/src/test/java/io/a2a/spec/SubTypeSerializationTest.java index a4571f923..0a5a898d7 100644 --- a/spec/src/test/java/io/a2a/spec/SubTypeSerializationTest.java +++ b/spec/src/test/java/io/a2a/spec/SubTypeSerializationTest.java @@ -1,15 +1,11 @@ package io.a2a.spec; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import io.a2a.json.JsonProcessingException; +import io.a2a.json.JsonUtil; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import java.util.HashMap; import java.util.Map; import java.util.stream.Stream; @@ -23,24 +19,12 @@ public class SubTypeSerializationTest { .status(new TaskStatus(TaskState.SUBMITTED)) .build(); - private static final TypeReference> MAP_TYPE_REFERENCE = new TypeReference<>() { - }; - - private static final ObjectMapper OBJECT_MAPPER; - - static { - OBJECT_MAPPER = new ObjectMapper(); - SimpleModule module = new SimpleModule(); - module.addAbstractTypeMapping(Map.class, SingleKeyHashMap.class); - OBJECT_MAPPER.registerModule(module); - OBJECT_MAPPER.registerModule(new JavaTimeModule()); - } - @ParameterizedTest @MethodSource("serializationTestCases") void testSubtypeSerialization(Object objectToSerialize, String typePropertyName, String expectedTypeValue) throws JsonProcessingException { - Map map = OBJECT_MAPPER.readValue(OBJECT_MAPPER.writeValueAsString(objectToSerialize), - MAP_TYPE_REFERENCE); + String json = JsonUtil.toJson(objectToSerialize); + @SuppressWarnings("unchecked") + Map map = JsonUtil.fromJson(json, Map.class); assertEquals(expectedTypeValue, map.get(typePropertyName)); } @@ -115,15 +99,4 @@ private static Stream serializationTestCases() { ); } - private static class SingleKeyHashMap extends HashMap { - @Override - public V put(K key, V value) { - if (containsKey(key)) { - throw new IllegalArgumentException("duplicate key " + key - + " with value " + get(key) + " and new value " + value); - } - return super.put(key, value); - } - } - } diff --git a/spec/src/test/java/io/a2a/spec/TaskDeserializationTest.java b/spec/src/test/java/io/a2a/spec/TaskDeserializationTest.java deleted file mode 100644 index fa67b59f5..000000000 --- a/spec/src/test/java/io/a2a/spec/TaskDeserializationTest.java +++ /dev/null @@ -1,90 +0,0 @@ -package io.a2a.spec; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -public class TaskDeserializationTest { - private final ObjectMapper objectMapper = new ObjectMapper(); - - @Test - void testTaskWithMissingHistoryAndArtifacts() throws Exception { - // JSON without history and artifacts fields (common server response) - String json = """ - { - "id": "task-123", - "contextId": "context-456", - "status": { - "state": "completed" - }, - "kind": "task" - } - """; - - Task task = objectMapper.readValue(json, Task.class); - - assertNotNull(task.getHistory(), "history should not be null"); - assertNotNull(task.getArtifacts(), "artifacts should not be null"); - - assertTrue(task.getHistory().isEmpty(), "history should be empty list when not provided"); - assertTrue(task.getArtifacts().isEmpty(), "artifacts should be empty list when not provided"); - } - - @Test - void testTaskWithExplicitNullValues() throws Exception { - // JSON with explicit null values - String json = """ - { - "id": "task-123", - "contextId": "context-456", - "status": { - "state": "completed" - }, - "history": null, - "artifacts": null, - "kind": "task" - } - """; - - Task task = objectMapper.readValue(json, Task.class); - - // Should never be null even with explicit null in JSON - assertNotNull(task.getHistory(), "history should not be null even when JSON contains null"); - assertNotNull(task.getArtifacts(), "artifacts should not be null even when JSON contains null"); - - assertTrue(task.getHistory().isEmpty()); - assertTrue(task.getArtifacts().isEmpty()); - } - - @Test - void testTaskWithPopulatedArrays() throws Exception { - String json = """ - { - "id": "task-123", - "contextId": "context-456", - "status": { - "state": "completed" - }, - "history": [ - { - "role": "user", - "parts": [{"kind": "text", "text": "hello"}], - "messageId": "msg-1", - "kind": "message" - } - ], - "artifacts": [], - "kind": "task" - } - """; - - Task task = objectMapper.readValue(json, Task.class); - - assertNotNull(task.getHistory()); - assertEquals(1, task.getHistory().size()); - - assertNotNull(task.getArtifacts()); - assertTrue(task.getArtifacts().isEmpty()); - } -} diff --git a/spec/src/test/java/io/a2a/spec/TaskSerializationTest.java b/spec/src/test/java/io/a2a/spec/TaskSerializationTest.java new file mode 100644 index 000000000..12bfcf508 --- /dev/null +++ b/spec/src/test/java/io/a2a/spec/TaskSerializationTest.java @@ -0,0 +1,713 @@ +package io.a2a.spec; + +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.time.OffsetDateTime; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import io.a2a.json.JsonProcessingException; +import io.a2a.json.JsonUtil; + +/** + * Tests for Task serialization and deserialization using Gson. + */ +class TaskSerializationTest { + + @Test + void testBasicTaskSerialization() throws JsonProcessingException { + // Create a basic task + Task task = new Task.Builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.SUBMITTED)) + .build(); + + // Serialize to JSON + String json = JsonUtil.toJson(task); + + // Verify JSON contains expected fields + assertNotNull(json); + assertTrue(json.contains("\"id\":\"task-123\"")); + assertTrue(json.contains("\"state\":\"submitted\"")); + + // Deserialize back to Task + Task deserialized = JsonUtil.fromJson(json, Task.class); + + // Verify deserialized task matches original + assertEquals(task.getId(), deserialized.getId()); + assertEquals(task.getStatus().state(), deserialized.getStatus().state()); + } + + @Test + void testTaskWithTimestamp() throws JsonProcessingException { + OffsetDateTime timestamp = OffsetDateTime.now(); + + Task task = new Task.Builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.WORKING, null, timestamp)) + .build(); + + // Serialize + String json = JsonUtil.toJson(task); + + // Deserialize + Task deserialized = JsonUtil.fromJson(json, Task.class); + + // Verify OffsetDateTime timestamp is preserved + assertNotNull(deserialized.getStatus().timestamp()); + assertEquals(task.getStatus().timestamp(), deserialized.getStatus().timestamp()); + } + + @Test + void testTaskWithArtifacts() throws JsonProcessingException { + Artifact artifact = new Artifact.Builder() + .artifactId("artifact-1") + .name("Test Artifact") + .description("Description of artifact") + .parts(List.of( + new TextPart("Hello"), + new TextPart("World") + )) + .build(); + + Task task = new Task.Builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.COMPLETED)) + .artifacts(List.of(artifact)) + .build(); + + // Serialize + String json = JsonUtil.toJson(task); + + // Verify JSON contains artifact data + assertTrue(json.contains("\"artifactId\":\"artifact-1\"")); + assertTrue(json.contains("Hello")); + assertTrue(json.contains("World")); + + // Deserialize + Task deserialized = JsonUtil.fromJson(json, Task.class); + + // Verify artifacts are preserved + assertNotNull(deserialized.getArtifacts()); + assertEquals(1, deserialized.getArtifacts().size()); + assertEquals("artifact-1", deserialized.getArtifacts().get(0).artifactId()); + assertEquals(2, deserialized.getArtifacts().get(0).parts().size()); + } + + @Test + void testTaskWithHistory() throws JsonProcessingException { + Message message = new Message.Builder() + .role(Message.Role.USER) + .parts(List.of(new TextPart("Test message"))) + .build(); + + Task task = new Task.Builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.WORKING)) + .history(List.of(message)) + .build(); + + // Serialize + String json = JsonUtil.toJson(task); + + // Verify JSON contains history data + assertTrue(json.contains("\"role\":\"user\"")); + assertTrue(json.contains("Test message")); + + // Deserialize + Task deserialized = JsonUtil.fromJson(json, Task.class); + + // Verify history is preserved + assertNotNull(deserialized.getHistory()); + assertEquals(1, deserialized.getHistory().size()); + assertEquals(Message.Role.USER, deserialized.getHistory().get(0).getRole()); + assertEquals(1, deserialized.getHistory().get(0).getParts().size()); + } + + @Test + void testTaskWithAllFields() throws JsonProcessingException { + OffsetDateTime timestamp = OffsetDateTime.now(); + + Task task = new Task.Builder() + .id("task-123") + .contextId("context-789") + .status(new TaskStatus(TaskState.WORKING, null, timestamp)) + .history(List.of( + new Message.Builder() + .role(Message.Role.USER) + .parts(List.of(new TextPart("User message"))) + .build(), + new Message.Builder() + .role(Message.Role.AGENT) + .parts(List.of(new TextPart("Agent response"))) + .build() + )) + .artifacts(List.of( + new Artifact.Builder() + .artifactId("artifact-1") + .parts(List.of(new TextPart("Artifact content"))) + .build() + )) + .metadata(Map.of("key1", "value1", "key2", 42)) + .build(); + + // Serialize + String json = JsonUtil.toJson(task); + + // Deserialize + Task deserialized = JsonUtil.fromJson(json, Task.class); + + // Verify all fields are preserved + assertEquals(task.getId(), deserialized.getId()); + assertEquals(task.getContextId(), deserialized.getContextId()); + assertEquals(task.getStatus().state(), deserialized.getStatus().state()); + assertEquals(task.getStatus().timestamp(), deserialized.getStatus().timestamp()); + assertEquals(task.getHistory().size(), deserialized.getHistory().size()); + assertEquals(task.getArtifacts().size(), deserialized.getArtifacts().size()); + assertNotNull(deserialized.getMetadata()); + assertEquals("value1", deserialized.getMetadata().get("key1")); + } + + @Test + void testTaskWithDifferentStates() throws JsonProcessingException { + for (TaskState state : TaskState.values()) { + Task task = new Task.Builder() + .id("task-" + state.asString()) + .contextId("context-123") + .status(new TaskStatus(state)) + .build(); + + // Serialize + String json = JsonUtil.toJson(task); + + // Verify state is serialized correctly + assertTrue(json.contains("\"state\":\"" + state.asString() + "\"")); + + // Deserialize + Task deserialized = JsonUtil.fromJson(json, Task.class); + + // Verify state is preserved + assertEquals(state, deserialized.getStatus().state()); + } + } + + @Test + void testTaskWithNullOptionalFields() throws JsonProcessingException { + Task task = new Task.Builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.SUBMITTED)) + // artifacts, history, metadata not set + .build(); + + // Serialize + String json = JsonUtil.toJson(task); + + // Deserialize + Task deserialized = JsonUtil.fromJson(json, Task.class); + + // Verify required fields are present + assertEquals("task-123", deserialized.getId()); + assertEquals("context-456", deserialized.getContextId()); + assertEquals(TaskState.SUBMITTED, deserialized.getStatus().state()); + + // Verify optional lists default to empty + assertNotNull(deserialized.getArtifacts()); + assertEquals(0, deserialized.getArtifacts().size()); + assertNotNull(deserialized.getHistory()); + assertEquals(0, deserialized.getHistory().size()); + } + + @Test + void testTaskWithFilePartBytes() throws JsonProcessingException { + FilePart filePart = new FilePart(new FileWithBytes("application/pdf", "document.pdf", "base64data")); + + Artifact artifact = new Artifact.Builder() + .artifactId("file-artifact") + .parts(List.of(filePart)) + .build(); + + Task task = new Task.Builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.COMPLETED)) + .artifacts(List.of(artifact)) + .build(); + + // Serialize + String json = JsonUtil.toJson(task); + + // Verify JSON contains file part data + assertTrue(json.contains("\"kind\":\"file\"")); + assertTrue(json.contains("document.pdf")); + assertTrue(json.contains("application/pdf")); + + // Deserialize + Task deserialized = JsonUtil.fromJson(json, Task.class); + + // Verify file part is preserved + Part part = deserialized.getArtifacts().get(0).parts().get(0); + assertTrue(part instanceof FilePart); + FilePart deserializedFilePart = (FilePart) part; + assertTrue(deserializedFilePart.getFile() instanceof FileWithBytes); + FileWithBytes fileWithBytes = (FileWithBytes) deserializedFilePart.getFile(); + assertEquals("document.pdf", fileWithBytes.name()); + assertEquals("application/pdf", fileWithBytes.mimeType()); + } + + @Test + void testTaskWithFilePartUri() throws JsonProcessingException { + FilePart filePart = new FilePart(new FileWithUri("image/png", "photo.png", "https://example.com/photo.png")); + + Artifact artifact = new Artifact.Builder() + .artifactId("uri-artifact") + .parts(List.of(filePart)) + .build(); + + Task task = new Task.Builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.COMPLETED)) + .artifacts(List.of(artifact)) + .build(); + + // Serialize + String json = JsonUtil.toJson(task); + + // Verify JSON contains URI + assertTrue(json.contains("https://example.com/photo.png")); + + // Deserialize + Task deserialized = JsonUtil.fromJson(json, Task.class); + + // Verify file part URI is preserved + Part part = deserialized.getArtifacts().get(0).parts().get(0); + assertTrue(part instanceof FilePart); + FilePart deserializedFilePart = (FilePart) part; + assertTrue(deserializedFilePart.getFile() instanceof FileWithUri); + FileWithUri fileWithUri = (FileWithUri) deserializedFilePart.getFile(); + assertEquals("https://example.com/photo.png", fileWithUri.uri()); + } + + @Test + void testTaskWithDataPart() throws JsonProcessingException { + DataPart dataPart = new DataPart(Map.of("temperature", 22.5, "humidity", 65)); + + Artifact artifact = new Artifact.Builder() + .artifactId("data-artifact") + .parts(List.of(dataPart)) + .build(); + + Task task = new Task.Builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.COMPLETED)) + .artifacts(List.of(artifact)) + .build(); + + // Serialize + String json = JsonUtil.toJson(task); + + // Verify JSON contains data part + assertTrue(json.contains("\"kind\":\"data\"")); + assertTrue(json.contains("temperature")); + + // Deserialize + Task deserialized = JsonUtil.fromJson(json, Task.class); + + // Verify data part is preserved + Part part = deserialized.getArtifacts().get(0).parts().get(0); + assertTrue(part instanceof DataPart); + DataPart deserializedDataPart = (DataPart) part; + assertNotNull(deserializedDataPart.getData()); + } + + @Test + void testTaskRoundTrip() throws JsonProcessingException { + // Create a comprehensive task with all part types + OffsetDateTime timestamp = OffsetDateTime.now(); + + Task original = new Task.Builder() + .id("task-123") + .contextId("context-789") + .status(new TaskStatus(TaskState.WORKING, null, timestamp)) + .history(List.of( + new Message.Builder() + .role(Message.Role.USER) + .parts(List.of( + new TextPart("Text"), + new FilePart(new FileWithBytes("text/plain", "file.txt", "data")), + new DataPart(Map.of("key", "value")) + )) + .build() + )) + .artifacts(List.of( + new Artifact.Builder() + .artifactId("artifact-1") + .parts(List.of(new TextPart("Content"))) + .build() + )) + .metadata(Map.of("meta1", "value1")) + .build(); + + // Serialize to JSON + String json = JsonUtil.toJson(original); + + // Deserialize back to Task + Task deserialized = JsonUtil.fromJson(json, Task.class); + + // Serialize again + String json2 = JsonUtil.toJson(deserialized); + + // Deserialize again + Task deserialized2 = JsonUtil.fromJson(json2, Task.class); + + // Verify multiple round-trips produce identical results + assertEquals(deserialized.getId(), deserialized2.getId()); + assertEquals(deserialized.getContextId(), deserialized2.getContextId()); + assertEquals(deserialized.getStatus().state(), deserialized2.getStatus().state()); + assertEquals(deserialized.getHistory().size(), deserialized2.getHistory().size()); + assertEquals(deserialized.getArtifacts().size(), deserialized2.getArtifacts().size()); + } + + @Test + void testTaskStatusWithMessage() throws JsonProcessingException { + Message statusMessage = new Message.Builder() + .role(Message.Role.AGENT) + .parts(List.of(new TextPart("Processing complete"))) + .build(); + + Task task = new Task.Builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.COMPLETED, statusMessage, null)) + .build(); + + // Serialize + String json = JsonUtil.toJson(task); + + // Verify JSON contains status message + assertTrue(json.contains("\"state\":\"completed\"")); + assertTrue(json.contains("Processing complete")); + + // Deserialize + Task deserialized = JsonUtil.fromJson(json, Task.class); + + // Verify status message is preserved + assertEquals(TaskState.COMPLETED, deserialized.getStatus().state()); + assertNotNull(deserialized.getStatus().message()); + assertEquals(Message.Role.AGENT, deserialized.getStatus().message().getRole()); + assertTrue(deserialized.getStatus().message().getParts().get(0) instanceof TextPart); + } + + @Test + void testDeserializeTaskFromJson() throws JsonProcessingException { + String json = """ + { + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "submitted" + }, + "kind": "task" + } + """; + + Task task = JsonUtil.fromJson(json, Task.class); + + assertEquals("task-123", task.getId()); + assertEquals("context-456", task.getContextId()); + assertEquals(TaskState.SUBMITTED, task.getStatus().state()); + assertNull(task.getStatus().message()); + // TaskStatus automatically sets timestamp to current time if not provided + assertNotNull(task.getStatus().timestamp()); + } + + @Test + void testDeserializeTaskWithArtifactsFromJson() throws JsonProcessingException { + String json = """ + { + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "completed" + }, + "artifacts": [ + { + "artifactId": "artifact-1", + "name": "Result", + "parts": [ + { + "kind": "text", + "text": "Hello World" + } + ] + } + ], + "kind": "task" + } + """; + + Task task = JsonUtil.fromJson(json, Task.class); + + assertEquals("task-123", task.getId()); + assertEquals(TaskState.COMPLETED, task.getStatus().state()); + assertEquals(1, task.getArtifacts().size()); + assertEquals("artifact-1", task.getArtifacts().get(0).artifactId()); + assertEquals("Result", task.getArtifacts().get(0).name()); + assertEquals(1, task.getArtifacts().get(0).parts().size()); + assertTrue(task.getArtifacts().get(0).parts().get(0) instanceof TextPart); + assertEquals("Hello World", ((TextPart) task.getArtifacts().get(0).parts().get(0)).getText()); + } + + @Test + void testDeserializeTaskWithFilePartBytesFromJson() throws JsonProcessingException { + String json = """ + { + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "completed" + }, + "artifacts": [ + { + "artifactId": "file-artifact", + "parts": [ + { + "kind": "file", + "file": { + "mimeType": "application/pdf", + "name": "document.pdf", + "bytes": "base64encodeddata" + } + } + ] + } + ], + "kind": "task" + } + """; + + Task task = JsonUtil.fromJson(json, Task.class); + + assertEquals("task-123", task.getId()); + assertEquals(1, task.getArtifacts().size()); + Part part = task.getArtifacts().get(0).parts().get(0); + assertTrue(part instanceof FilePart); + FilePart filePart = (FilePart) part; + assertTrue(filePart.getFile() instanceof FileWithBytes); + FileWithBytes fileWithBytes = (FileWithBytes) filePart.getFile(); + assertEquals("application/pdf", fileWithBytes.mimeType()); + assertEquals("document.pdf", fileWithBytes.name()); + assertEquals("base64encodeddata", fileWithBytes.bytes()); + } + + @Test + void testDeserializeTaskWithFilePartUriFromJson() throws JsonProcessingException { + String json = """ + { + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "completed" + }, + "artifacts": [ + { + "artifactId": "uri-artifact", + "parts": [ + { + "kind": "file", + "file": { + "mimeType": "image/png", + "name": "photo.png", + "uri": "https://example.com/photo.png" + } + } + ] + } + ], + "kind": "task" + } + """; + + Task task = JsonUtil.fromJson(json, Task.class); + + assertEquals("task-123", task.getId()); + Part part = task.getArtifacts().get(0).parts().get(0); + assertTrue(part instanceof FilePart); + FilePart filePart = (FilePart) part; + assertTrue(filePart.getFile() instanceof FileWithUri); + FileWithUri fileWithUri = (FileWithUri) filePart.getFile(); + assertEquals("image/png", fileWithUri.mimeType()); + assertEquals("photo.png", fileWithUri.name()); + assertEquals("https://example.com/photo.png", fileWithUri.uri()); + } + + @Test + void testDeserializeTaskWithDataPartFromJson() throws JsonProcessingException { + String json = """ + { + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "completed" + }, + "artifacts": [ + { + "artifactId": "data-artifact", + "parts": [ + { + "kind": "data", + "data": { + "temperature": 22.5, + "humidity": 65 + } + } + ] + } + ], + "kind": "task" + } + """; + + Task task = JsonUtil.fromJson(json, Task.class); + + assertEquals("task-123", task.getId()); + Part part = task.getArtifacts().get(0).parts().get(0); + assertTrue(part instanceof DataPart); + DataPart dataPart = (DataPart) part; + assertNotNull(dataPart.getData()); + } + + @Test + void testDeserializeTaskWithHistoryFromJson() throws JsonProcessingException { + String json = """ + { + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "working" + }, + "history": [ + { + "role": "user", + "parts": [ + { + "kind": "text", + "text": "User message" + } + ] + }, + { + "role": "agent", + "parts": [ + { + "kind": "text", + "text": "Agent response" + } + ] + } + ], + "kind": "task" + } + """; + + Task task = JsonUtil.fromJson(json, Task.class); + + assertEquals("task-123", task.getId()); + assertEquals(2, task.getHistory().size()); + assertEquals(Message.Role.USER, task.getHistory().get(0).getRole()); + assertEquals(Message.Role.AGENT, task.getHistory().get(1).getRole()); + assertTrue(task.getHistory().get(0).getParts().get(0) instanceof TextPart); + assertEquals("User message", ((TextPart) task.getHistory().get(0).getParts().get(0)).getText()); + } + + @Test + void testDeserializeTaskWithTimestampFromJson() throws JsonProcessingException { + String json = """ + { + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "working", + "timestamp": "2023-10-01T12:00:00.234-05:00" + }, + "kind": "task" + } + """; + + Task task = JsonUtil.fromJson(json, Task.class); + + assertEquals("task-123", task.getId()); + assertEquals(TaskState.WORKING, task.getStatus().state()); + assertNotNull(task.getStatus().timestamp()); + assertEquals("2023-10-01T12:00:00.234-05:00", task.getStatus().timestamp().toString()); + } + + @Test + void testDeserializeTaskWithMetadataFromJson() throws JsonProcessingException { + String json = """ + { + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "completed" + }, + "metadata": { + "key1": "value1", + "key2": 42 + }, + "kind": "task" + } + """; + + Task task = JsonUtil.fromJson(json, Task.class); + + assertEquals("task-123", task.getId()); + assertNotNull(task.getMetadata()); + assertEquals("value1", task.getMetadata().get("key1")); + } + + @Test + void testTaskWithMixedPartTypes() throws JsonProcessingException { + Artifact artifact = new Artifact.Builder() + .artifactId("mixed-artifact") + .parts(List.of( + new TextPart("Text content"), + new FilePart(new FileWithBytes("application/json", "data.json", "{}")), + new DataPart(Map.of("result", 42)), + new FilePart(new FileWithUri("image/png", "image.png", "https://example.com/img.png")) + )) + .build(); + + Task task = new Task.Builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.COMPLETED)) + .artifacts(List.of(artifact)) + .build(); + + // Serialize + String json = JsonUtil.toJson(task); + + // Deserialize + Task deserialized = JsonUtil.fromJson(json, Task.class); + + // Verify all part types are preserved + List> parts = deserialized.getArtifacts().get(0).parts(); + assertEquals(4, parts.size()); + assertTrue(parts.get(0) instanceof TextPart); + assertTrue(parts.get(1) instanceof FilePart); + assertTrue(parts.get(2) instanceof DataPart); + assertTrue(parts.get(3) instanceof FilePart); + } +} diff --git a/spec/src/test/java/io/a2a/spec/TaskStatusTest.java b/spec/src/test/java/io/a2a/spec/TaskStatusTest.java deleted file mode 100644 index 7c4a9db8a..000000000 --- a/spec/src/test/java/io/a2a/spec/TaskStatusTest.java +++ /dev/null @@ -1,94 +0,0 @@ -package io.a2a.spec; - -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import org.junit.jupiter.api.Test; - -import java.time.OffsetDateTime; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class TaskStatusTest { - - private static final ObjectMapper OBJECT_MAPPER; - - private static final String REPLACE_TIMESTAMP_PATTERN = ".*\"timestamp\":\"([^\"]+)\",?.*"; - - static { - OBJECT_MAPPER = new ObjectMapper(); - OBJECT_MAPPER.registerModule(new JavaTimeModule()); - OBJECT_MAPPER.configure(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE, false); - } - - @Test - public void testTaskStatusWithSetTimestamp() { - TaskState state = TaskState.WORKING; - OffsetDateTime offsetDateTime = OffsetDateTime.parse("2023-10-01T12:00:00Z"); - TaskStatus status = new TaskStatus(state, offsetDateTime); - - assertNotNull(status.timestamp()); - assertEquals(offsetDateTime, status.timestamp()); - } - - @Test - public void testTaskStatusWithProvidedTimestamp() { - OffsetDateTime providedTimestamp = OffsetDateTime.parse("2024-01-01T00:00:00Z"); - TaskState state = TaskState.COMPLETED; - TaskStatus status = new TaskStatus(state, providedTimestamp); - - assertEquals(providedTimestamp, status.timestamp()); - } - - @Test - public void testTaskStatusSerializationUsesISO8601Format() throws Exception { - OffsetDateTime expectedTimestamp = OffsetDateTime.parse("2023-10-01T12:00:00.234-05:00"); - TaskState state = TaskState.WORKING; - TaskStatus status = new TaskStatus(state, expectedTimestamp); - - String json = OBJECT_MAPPER.writeValueAsString(status); - - String expectedJson = "{\"state\":\"working\",\"timestamp\":\"2023-10-01T12:00:00.234-05:00\"}"; - assertEquals(expectedJson, json); - } - - @Test - public void testTaskStatusDeserializationWithValidISO8601Format() throws Exception { - String validJson = "{" - + "\"state\": \"auth-required\"," - + "\"timestamp\": \"2023-10-01T12:00:00.10+03:00\"" - + "}"; - - TaskStatus result = OBJECT_MAPPER.readValue(validJson, TaskStatus.class); - assertEquals(TaskState.AUTH_REQUIRED, result.state()); - assertNotNull(result.timestamp()); - assertEquals(OffsetDateTime.parse("2023-10-01T12:00:00.100+03:00"), result.timestamp()); - } - - @Test - public void testTaskStatusDeserializationWithInvalidISO8601FormatFails() { - String invalidJson = "{" - + "\"state\": \"completed\"," - + "\"timestamp\": \"2023/10/01 12:00:00\"" - + "}"; - - assertThrows( - com.fasterxml.jackson.databind.exc.InvalidFormatException.class, - () -> OBJECT_MAPPER.readValue(invalidJson, TaskStatus.class) - ); - } - - @Test - public void testTaskStatusJsonTimestampMatchesISO8601Regex() throws Exception { - TaskState state = TaskState.WORKING; - OffsetDateTime expectedTimestamp = OffsetDateTime.parse("2023-10-01T12:00:00.234Z"); - TaskStatus status = new TaskStatus(state, expectedTimestamp); - - String json = OBJECT_MAPPER.writeValueAsString(status); - - String timestampValue = json.replaceAll(REPLACE_TIMESTAMP_PATTERN, "$1"); - assertEquals(expectedTimestamp, OffsetDateTime.parse(timestampValue)); - } -} diff --git a/tck/pom.xml b/tck/pom.xml index 6b4002b03..06aab7b63 100644 --- a/tck/pom.xml +++ b/tck/pom.xml @@ -30,7 +30,7 @@
io.quarkus - quarkus-rest-jackson + quarkus-rest provided diff --git a/tests/server-common/src/test/java/io/a2a/server/apps/common/A2AGsonObjectMapper.java b/tests/server-common/src/test/java/io/a2a/server/apps/common/A2AGsonObjectMapper.java new file mode 100644 index 000000000..c456ff3b1 --- /dev/null +++ b/tests/server-common/src/test/java/io/a2a/server/apps/common/A2AGsonObjectMapper.java @@ -0,0 +1,38 @@ +/* + * Copyright The WildFly Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package io.a2a.server.apps.common; + +import io.a2a.json.JsonProcessingException; +import io.a2a.json.JsonUtil; +import io.restassured.mapper.ObjectMapper; +import io.restassured.mapper.ObjectMapperDeserializationContext; +import io.restassured.mapper.ObjectMapperSerializationContext; + + +public class A2AGsonObjectMapper implements ObjectMapper { + public static final A2AGsonObjectMapper INSTANCE = new A2AGsonObjectMapper(); + + private A2AGsonObjectMapper() { + } + + @Override + public Object deserialize(ObjectMapperDeserializationContext context) { + try { + return JsonUtil.fromJson(context.getDataToDeserialize().asString(), context.getType()); + } catch (JsonProcessingException ex) { + throw new RuntimeException(ex); + } + } + + @Override + public Object serialize(ObjectMapperSerializationContext context) { + try { + return JsonUtil.toJson(context.getObjectToSerialize()); + } catch (JsonProcessingException ex) { + + throw new RuntimeException(ex); + } + } +} diff --git a/tests/server-common/src/test/java/io/a2a/server/apps/common/AbstractA2AServerTest.java b/tests/server-common/src/test/java/io/a2a/server/apps/common/AbstractA2AServerTest.java index 7fcf32cb4..be5d8403a 100644 --- a/tests/server-common/src/test/java/io/a2a/server/apps/common/AbstractA2AServerTest.java +++ b/tests/server-common/src/test/java/io/a2a/server/apps/common/AbstractA2AServerTest.java @@ -1,6 +1,5 @@ package io.a2a.server.apps.common; -import static io.restassured.RestAssured.given; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; @@ -31,7 +30,7 @@ import jakarta.ws.rs.core.MediaType; -import com.fasterxml.jackson.core.JsonProcessingException; +import io.a2a.json.JsonProcessingException; import io.a2a.client.Client; import io.a2a.client.ClientBuilder; import io.a2a.client.ClientEvent; @@ -72,7 +71,11 @@ import io.a2a.spec.TextPart; import io.a2a.spec.TransportProtocol; import io.a2a.spec.UnsupportedOperationError; +import io.a2a.json.JsonUtil; import io.a2a.util.Utils; +import io.restassured.RestAssured; +import io.restassured.config.ObjectMapperConfig; +import io.restassured.specification.RequestSpecification; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; @@ -114,6 +117,13 @@ public abstract class AbstractA2AServerTest { .build(); public static final String APPLICATION_JSON = "application/json"; + public static RequestSpecification given() { + return RestAssured.given() + .config(RestAssured.config() + .objectMapperConfig(new ObjectMapperConfig(A2AGsonObjectMapper.INSTANCE))); +} + + protected final int serverPort; private Client client; private Client nonStreamingClient; @@ -1355,7 +1365,7 @@ private CompletableFuture>> initialiseStreamingReque // Create the request HttpRequest.Builder builder = HttpRequest.newBuilder() .uri(URI.create("http://localhost:" + serverPort + "/")) - .POST(HttpRequest.BodyPublishers.ofString(Utils.OBJECT_MAPPER.writeValueAsString(request))) + .POST(HttpRequest.BodyPublishers.ofString(JsonUtil.toJson(request))) .header("Content-Type", APPLICATION_JSON); if (mediaType != null) { builder.header("Accept", mediaType); @@ -1370,7 +1380,7 @@ private CompletableFuture>> initialiseStreamingReque private SendStreamingMessageResponse extractJsonResponseFromSseLine(String line) throws JsonProcessingException { line = extractSseData(line); if (line != null) { - return Utils.OBJECT_MAPPER.readValue(line, SendStreamingMessageResponse.class); + return JsonUtil.fromJson(line, SendStreamingMessageResponse.class); } return null; } @@ -1407,7 +1417,7 @@ protected void saveTaskInTaskStore(Task task) throws Exception { .build(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:" + serverPort + "/test/task")) - .POST(HttpRequest.BodyPublishers.ofString(Utils.OBJECT_MAPPER.writeValueAsString(task))) + .POST(HttpRequest.BodyPublishers.ofString(JsonUtil.toJson(task))) .header("Content-Type", APPLICATION_JSON) .build(); @@ -1433,7 +1443,7 @@ protected Task getTaskFromTaskStore(String taskId) throws Exception { if (response.statusCode() != 200) { throw new RuntimeException(String.format("Getting task failed! Status: %d, Body: %s", response.statusCode(), response.body())); } - return Utils.OBJECT_MAPPER.readValue(response.body(), Task.TYPE_REFERENCE); + return JsonUtil.fromJson(response.body(), Task.class); } protected void deleteTaskInTaskStore(String taskId) throws Exception { @@ -1480,7 +1490,7 @@ protected void enqueueEventOnServer(Event event) throws Exception { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:" + serverPort + "/" + path)) .header("Content-Type", APPLICATION_JSON) - .POST(HttpRequest.BodyPublishers.ofString(Utils.OBJECT_MAPPER.writeValueAsString(event))) + .POST(HttpRequest.BodyPublishers.ofString(JsonUtil.toJson(event))) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); @@ -1569,7 +1579,7 @@ protected void savePushNotificationConfigInStore(String taskId, PushNotification .build(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:" + serverPort + "/test/task/" + taskId)) - .POST(HttpRequest.BodyPublishers.ofString(Utils.OBJECT_MAPPER.writeValueAsString(notificationConfig))) + .POST(HttpRequest.BodyPublishers.ofString(JsonUtil.toJson(notificationConfig))) .header("Content-Type", APPLICATION_JSON) .build(); diff --git a/tests/server-common/src/test/java/io/a2a/server/apps/common/TestHttpClient.java b/tests/server-common/src/test/java/io/a2a/server/apps/common/TestHttpClient.java index f161307aa..7cc62dcfe 100644 --- a/tests/server-common/src/test/java/io/a2a/server/apps/common/TestHttpClient.java +++ b/tests/server-common/src/test/java/io/a2a/server/apps/common/TestHttpClient.java @@ -13,8 +13,9 @@ import io.a2a.client.http.A2AHttpClient; import io.a2a.client.http.A2AHttpResponse; +import io.a2a.json.JsonProcessingException; import io.a2a.spec.Task; -import io.a2a.util.Utils; +import io.a2a.json.JsonUtil; import java.util.Map; @Dependent @@ -48,7 +49,11 @@ public PostBuilder body(String body) { @Override public A2AHttpResponse post() throws IOException, InterruptedException { - tasks.add(Utils.OBJECT_MAPPER.readValue(body, Task.TYPE_REFERENCE)); + try { + tasks.add(JsonUtil.fromJson(body, Task.class)); + } catch (JsonProcessingException e) { + throw new IOException("Failed to parse task JSON", e); + } try { return new A2AHttpResponse() { @Override diff --git a/transport/jsonrpc/src/main/java/io/a2a/transport/jsonrpc/handler/JSONRPCHandler.java b/transport/jsonrpc/src/main/java/io/a2a/transport/jsonrpc/handler/JSONRPCHandler.java index 9a9d5eef1..ef577aaa5 100644 --- a/transport/jsonrpc/src/main/java/io/a2a/transport/jsonrpc/handler/JSONRPCHandler.java +++ b/transport/jsonrpc/src/main/java/io/a2a/transport/jsonrpc/handler/JSONRPCHandler.java @@ -229,7 +229,7 @@ public GetAuthenticatedExtendedCardResponse onGetAuthenticatedExtendedCardReques GetAuthenticatedExtendedCardRequest request, ServerCallContext context) { if ( !agentCard.supportsAuthenticatedExtendedCard() || !extendedAgentCard.isResolvable()) { return new GetAuthenticatedExtendedCardResponse(request.getId(), - new AuthenticatedExtendedCardNotConfiguredError()); + new AuthenticatedExtendedCardNotConfiguredError(null, "Authenticated Extended Card not configured", null)); } try { return new GetAuthenticatedExtendedCardResponse(request.getId(), extendedAgentCard.get()); diff --git a/transport/rest/pom.xml b/transport/rest/pom.xml index 53344385d..c4d1c5ff6 100644 --- a/transport/rest/pom.xml +++ b/transport/rest/pom.xml @@ -51,10 +51,6 @@ mockito-core test - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - com.google.protobuf protobuf-java-util diff --git a/transport/rest/src/main/java/io/a2a/transport/rest/handler/RestHandler.java b/transport/rest/src/main/java/io/a2a/transport/rest/handler/RestHandler.java index 85d307f53..da4bcba42 100644 --- a/transport/rest/src/main/java/io/a2a/transport/rest/handler/RestHandler.java +++ b/transport/rest/src/main/java/io/a2a/transport/rest/handler/RestHandler.java @@ -2,7 +2,8 @@ import static io.a2a.server.util.async.AsyncUtils.createTubeConfig; -import com.fasterxml.jackson.core.JacksonException; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.util.JsonFormat; import io.a2a.grpc.utils.ProtoUtils; @@ -41,7 +42,7 @@ import io.a2a.spec.TaskQueryParams; import io.a2a.spec.UnsupportedOperationError; import io.a2a.server.util.async.Internal; -import io.a2a.util.Utils; +import io.a2a.json.JsonUtil; import jakarta.enterprise.inject.Instance; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; @@ -238,8 +239,8 @@ private void parseRequestBody(String body, com.google.protobuf.Message.Builder b private void validate(String json) { try { - Utils.OBJECT_MAPPER.readTree(json); - } catch (JacksonException e) { + JsonParser.parseString(json); + } catch (JsonSyntaxException e) { throw new JSONParseError(JSONParseError.DEFAULT_CODE, "Failed to parse json", e.getMessage()); } } @@ -346,9 +347,9 @@ private int mapErrorToHttpStatus(JSONRPCError error) { public HTTPRestResponse getAuthenticatedExtendedCard() { try { if (!agentCard.supportsAuthenticatedExtendedCard() || extendedAgentCard == null || !extendedAgentCard.isResolvable()) { - throw new AuthenticatedExtendedCardNotConfiguredError(); + throw new AuthenticatedExtendedCardNotConfiguredError(null, "Authenticated Extended Card not configured", null); } - return new HTTPRestResponse(200, "application/json", Utils.OBJECT_MAPPER.writeValueAsString(extendedAgentCard.get())); + return new HTTPRestResponse(200, "application/json", JsonUtil.toJson(extendedAgentCard.get())); } catch (JSONRPCError e) { return createErrorResponse(e); } catch (Throwable t) { @@ -358,7 +359,7 @@ public HTTPRestResponse getAuthenticatedExtendedCard() { public HTTPRestResponse getAgentCard() { try { - return new HTTPRestResponse(200, "application/json", Utils.OBJECT_MAPPER.writeValueAsString(agentCard)); + return new HTTPRestResponse(200, "application/json", JsonUtil.toJson(agentCard)); } catch (Throwable t) { return createErrorResponse(500, new InternalError(t.getMessage())); }