+ * 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. + * + *
+ * Incoming JSON-RPC Request + * ↓ parseRequestBody(String) + * Validate version, id, method + * ↓ parseMethodRequest() + * Parse params → Proto Builder + * ↓ ProtoUtils.FromProto.* + * Create JSONRPCRequest<?> with spec objects + *+ * + *
+ * 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 + *+ * + *
+ * Proto MessageOrBuilder + * ↓ JsonFormat.printer() + * Proto JSON string + * ↓ Gson JsonWriter + * Complete JSON-RPC envelope + *+ * + *
{@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 @@
+ * 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
+ * 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
+ * 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:
+ *
+ * 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
+ * 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:
+ *
+ * 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
+ * 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
+ * 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
+ * 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
+ * Discriminates based on the {@code "kind"} field:
+ *
+ * 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
+ * Discriminates based on the {@code "type"} field:
+ *
+ * This adapter handles polymorphic deserialization for the sealed FileContent interface,
+ * which permits two implementations:
+ *
+ * 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
+ * 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:
+ *
+ * Usage example:
+ *
+ *
+ *
+ *
+ *
+ * @see JSONRPCError
+ */
+ static class JSONRPCErrorTypeAdapter extends TypeAdapter
+ *
+ */
+ static class EventKindTypeAdapter extends TypeAdapter
+ *
+ */
+ static class SecuritySchemeTypeAdapter extends TypeAdapter
+ *
+ *
+ *
+ * {@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