Skip to content

Commit 7ca6b8b

Browse files
committed
refactor: Complete migration from Jackson to Gson across codebase
Replace Jackson ObjectMapper with Gson throughout the codebase to eliminate Jackson dependency and provide custom serialization control. Key Changes: - Created JsonUtil.java with Gson-based OBJECT_MAPPER singleton - Implemented 9 custom TypeAdapters for complex type handling: * OffsetDateTimeTypeAdapter - ISO-8601 datetime serialization * JSONRPCErrorTypeAdapter - Polymorphic error deserialization (12 error types) * ThrowableTypeAdapter - Avoids Java 17+ reflection restrictions * TaskStateTypeAdapter - Wire format enum serialization * RoleTypeAdapter - Message.Role enum handling * PartKindTypeAdapter - Part.Kind enum handling * PartTypeAdapter - Polymorphic Part deserialization (Text/File/Data) * StreamingEventKindTypeAdapter - Event type deserialization * FileContentTypeAdapter - FileWithBytes/FileWithUri discrimination - Added TaskSerializationTest with 21 comprehensive tests covering: * Round-trip serialization for all Task components * Direct JSON deserialization tests * All Part types and FileContent variants - Removed Jackson-specific deserializers and annotations - Updated all usages across 142 files (client, server, extras, tests) - Removed Jackson dependencies from spec/pom.xml Technical Details: - Thread-safe singleton OBJECT_MAPPER with all adapters registered - Handles polymorphic types via two-pass parsing strategy - Maintains wire format compatibility with existing JSON-RPC protocol - Preserves null-safety with NullAway/JSpecify conventions Breaking Changes: None - wire format remains compatible Signed-off-by: Emmanuel Hugonnet <[email protected]>
1 parent 46793cf commit 7ca6b8b

File tree

142 files changed

+1988
-1743
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

142 files changed

+1988
-1743
lines changed

client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransport.java

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import java.util.concurrent.atomic.AtomicReference;
1010
import java.util.function.Consumer;
1111

12-
import com.fasterxml.jackson.core.JsonProcessingException;
12+
import io.a2a.json.JsonProcessingException;
1313
import com.google.protobuf.MessageOrBuilder;
1414
import io.a2a.client.http.A2ACardResolver;
1515
import io.a2a.client.http.A2AHttpClient;
@@ -100,7 +100,7 @@ public EventKind sendMessage(MessageSendParams request, @Nullable ClientCallCont
100100
return response.getResult();
101101
} catch (A2AClientException e) {
102102
throw e;
103-
} catch (IOException | InterruptedException e) {
103+
} catch (IOException | InterruptedException | JsonProcessingException e) {
104104
throw new A2AClientException("Failed to send message: " + e, e);
105105
}
106106
}
@@ -129,6 +129,8 @@ public void sendMessageStreaming(MessageSendParams request, Consumer<StreamingEv
129129
throw new A2AClientException("Failed to send streaming message request: " + e, e);
130130
} catch (InterruptedException e) {
131131
throw new A2AClientException("Send streaming message request timed out: " + e, e);
132+
} catch (JsonProcessingException e) {
133+
throw new A2AClientException("Failed to process JSON for streaming message request: " + e, e);
132134
}
133135
}
134136

@@ -144,7 +146,7 @@ public Task getTask(TaskQueryParams request, @Nullable ClientCallContext context
144146
return response.getResult();
145147
} catch (A2AClientException e) {
146148
throw e;
147-
} catch (IOException | InterruptedException e) {
149+
} catch (IOException | InterruptedException | JsonProcessingException e) {
148150
throw new A2AClientException("Failed to get task: " + e, e);
149151
}
150152
}
@@ -161,7 +163,7 @@ public Task cancelTask(TaskIdParams request, @Nullable ClientCallContext context
161163
return response.getResult();
162164
} catch (A2AClientException e) {
163165
throw e;
164-
} catch (IOException | InterruptedException e) {
166+
} catch (IOException | InterruptedException | JsonProcessingException e) {
165167
throw new A2AClientException("Failed to cancel task: " + e, e);
166168
}
167169
}
@@ -175,7 +177,7 @@ public ListTasksResult listTasks(ListTasksParams request, @Nullable ClientCallCo
175177
String httpResponseBody = sendPostRequest(payloadAndHeaders, ListTasksRequest.METHOD);
176178
ListTasksResponse response = unmarshalResponse(httpResponseBody, ListTasksRequest.METHOD);
177179
return response.getResult();
178-
} catch (IOException | InterruptedException e) {
180+
} catch (IOException | InterruptedException | JsonProcessingException e) {
179181
throw new A2AClientException("Failed to list tasks: " + e, e);
180182
}
181183
}
@@ -194,7 +196,7 @@ public TaskPushNotificationConfig setTaskPushNotificationConfiguration(TaskPushN
194196
return response.getResult();
195197
} catch (A2AClientException e) {
196198
throw e;
197-
} catch (IOException | InterruptedException e) {
199+
} catch (IOException | InterruptedException | JsonProcessingException e) {
198200
throw new A2AClientException("Failed to set task push notification config: " + e, e);
199201
}
200202
}
@@ -213,7 +215,7 @@ public TaskPushNotificationConfig getTaskPushNotificationConfiguration(GetTaskPu
213215
return response.getResult();
214216
} catch (A2AClientException e) {
215217
throw e;
216-
} catch (IOException | InterruptedException e) {
218+
} catch (IOException | InterruptedException | JsonProcessingException e) {
217219
throw new A2AClientException("Failed to get task push notification config: " + e, e);
218220
}
219221
}
@@ -233,7 +235,7 @@ public List<TaskPushNotificationConfig> listTaskPushNotificationConfigurations(
233235
return response.getResult();
234236
} catch (A2AClientException e) {
235237
throw e;
236-
} catch (IOException | InterruptedException e) {
238+
} catch (IOException | InterruptedException | JsonProcessingException e) {
237239
throw new A2AClientException("Failed to list task push notification configs: " + e, e);
238240
}
239241
}
@@ -251,7 +253,7 @@ public void deleteTaskPushNotificationConfigurations(DeleteTaskPushNotificationC
251253
// Response validated (no error), but no result to return
252254
} catch (A2AClientException e) {
253255
throw e;
254-
} catch (IOException | InterruptedException e) {
256+
} catch (IOException | InterruptedException | JsonProcessingException e) {
255257
throw new A2AClientException("Failed to delete task push notification configs: " + e, e);
256258
}
257259
}
@@ -281,6 +283,8 @@ public void resubscribe(TaskIdParams request, Consumer<StreamingEventKind> event
281283
throw new A2AClientException("Failed to send task resubscription request: " + e, e);
282284
} catch (InterruptedException e) {
283285
throw new A2AClientException("Task resubscription request timed out: " + e, e);
286+
} catch (JsonProcessingException e) {
287+
throw new A2AClientException("Failed to process JSON for task resubscription request: " + e, e);
284288
}
285289
}
286290

@@ -312,7 +316,7 @@ public AgentCard getAgentCard(@Nullable ClientCallContext context) throws A2ACli
312316
agentCard = response.getResult();
313317
needsExtendedCard = false;
314318
return agentCard;
315-
} catch (IOException | InterruptedException e) {
319+
} catch (IOException | InterruptedException | JsonProcessingException e) {
316320
throw new A2AClientException("Failed to get authenticated extended agent card: " + e, e);
317321
}
318322
} catch(A2AClientError e){
@@ -337,7 +341,7 @@ private PayloadAndHeaders applyInterceptors(String methodName, @Nullable Object
337341
return payloadAndHeaders;
338342
}
339343

340-
private String sendPostRequest(PayloadAndHeaders payloadAndHeaders, String method) throws IOException, InterruptedException {
344+
private String sendPostRequest(PayloadAndHeaders payloadAndHeaders, String method) throws IOException, InterruptedException, JsonProcessingException {
341345
A2AHttpClient.PostBuilder builder = createPostBuilder(payloadAndHeaders,method);
342346
A2AHttpResponse response = builder.post();
343347
if (!response.success()) {

client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/sse/SSEEventListener.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package io.a2a.client.transport.jsonrpc.sse;
22

3-
import com.fasterxml.jackson.core.JsonProcessingException;
3+
import io.a2a.json.JsonProcessingException;
44
import io.a2a.spec.JSONRPCError;
55
import io.a2a.spec.StreamingEventKind;
66
import io.a2a.spec.TaskStatusUpdateEvent;

client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestErrorMapper.java

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
package io.a2a.client.transport.rest;
22

3-
import com.fasterxml.jackson.core.JsonProcessingException;
4-
import com.fasterxml.jackson.databind.JsonNode;
5-
import com.fasterxml.jackson.databind.ObjectMapper;
6-
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
3+
import com.google.gson.JsonObject;
74
import io.a2a.client.http.A2AHttpResponse;
5+
import io.a2a.json.JsonProcessingException;
6+
import io.a2a.json.JsonUtil;
87
import io.a2a.spec.A2AClientException;
98
import io.a2a.spec.AuthenticatedExtendedCardNotConfiguredError;
109
import io.a2a.spec.ContentTypeNotSupportedError;
@@ -26,18 +25,16 @@
2625
*/
2726
public class RestErrorMapper {
2827

29-
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().registerModule(new JavaTimeModule());
30-
3128
public static A2AClientException mapRestError(A2AHttpResponse response) {
3229
return RestErrorMapper.mapRestError(response.body(), response.status());
3330
}
3431

3532
public static A2AClientException mapRestError(String body, int code) {
3633
try {
3734
if (body != null && !body.isBlank()) {
38-
JsonNode node = OBJECT_MAPPER.readTree(body);
39-
String className = node.findValue("error").asText();
40-
String errorMessage = node.findValue("message").asText();
35+
JsonObject node = JsonUtil.fromJson(body, JsonObject.class);
36+
String className = node.has("error") ? node.get("error").getAsString() : "";
37+
String errorMessage = node.has("message") ? node.get("message").getAsString() : "";
4138
return mapRestError(className, errorMessage, code);
4239
}
4340
return mapRestError("", "", code);
@@ -50,7 +47,7 @@ public static A2AClientException mapRestError(String body, int code) {
5047
public static A2AClientException mapRestError(String className, String errorMessage, int code) {
5148
return switch (className) {
5249
case "io.a2a.spec.TaskNotFoundError" -> new A2AClientException(errorMessage, new TaskNotFoundError());
53-
case "io.a2a.spec.AuthenticatedExtendedCardNotConfiguredError" -> new A2AClientException(errorMessage, new AuthenticatedExtendedCardNotConfiguredError());
50+
case "io.a2a.spec.AuthenticatedExtendedCardNotConfiguredError" -> new A2AClientException(errorMessage, new AuthenticatedExtendedCardNotConfiguredError(null, errorMessage, null));
5451
case "io.a2a.spec.ContentTypeNotSupportedError" -> new A2AClientException(errorMessage, new ContentTypeNotSupportedError(null, null, errorMessage));
5552
case "io.a2a.spec.InternalError" -> new A2AClientException(errorMessage, new InternalError(errorMessage));
5653
case "io.a2a.spec.InvalidAgentResponseError" -> new A2AClientException(errorMessage, new InvalidAgentResponseError(null, null, errorMessage));

client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransport.java

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import java.util.function.Consumer;
1414
import java.util.logging.Logger;
1515

16-
import com.fasterxml.jackson.core.JsonProcessingException;
16+
import io.a2a.json.JsonProcessingException;
1717
import com.google.protobuf.InvalidProtocolBufferException;
1818
import com.google.protobuf.MessageOrBuilder;
1919
import com.google.protobuf.util.JsonFormat;
@@ -27,6 +27,7 @@
2727
import io.a2a.client.transport.spi.interceptors.ClientCallInterceptor;
2828
import io.a2a.client.transport.spi.interceptors.PayloadAndHeaders;
2929
import io.a2a.grpc.utils.ProtoUtils;
30+
import io.a2a.json.JsonUtil;
3031
import io.a2a.spec.A2AClientError;
3132
import io.a2a.spec.A2AClientException;
3233
import io.a2a.spec.AgentCard;
@@ -94,7 +95,7 @@ public EventKind sendMessage(MessageSendParams messageSendParams, @Nullable Clie
9495
throw new A2AClientException("Failed to send message, wrong response:" + httpResponseBody);
9596
} catch (A2AClientException e) {
9697
throw e;
97-
} catch (IOException | InterruptedException e) {
98+
} catch (IOException | InterruptedException | JsonProcessingException e) {
9899
throw new A2AClientException("Failed to send message: " + e, e);
99100
}
100101
}
@@ -121,6 +122,8 @@ public void sendMessageStreaming(MessageSendParams messageSendParams, Consumer<S
121122
throw new A2AClientException("Failed to send streaming message request: " + e, e);
122123
} catch (InterruptedException e) {
123124
throw new A2AClientException("Send streaming message request timed out: " + e, e);
125+
} catch (JsonProcessingException e) {
126+
throw new A2AClientException("Failed to process JSON for streaming message request: " + e, e);
124127
}
125128
}
126129

@@ -173,7 +176,7 @@ public Task cancelTask(TaskIdParams taskIdParams, @Nullable ClientCallContext co
173176
return ProtoUtils.FromProto.task(responseBuilder);
174177
} catch (A2AClientException e) {
175178
throw e;
176-
} catch (IOException | InterruptedException e) {
179+
} catch (IOException | InterruptedException | JsonProcessingException e) {
177180
throw new A2AClientException("Failed to cancel task: " + e, e);
178181
}
179182
}
@@ -280,7 +283,7 @@ public TaskPushNotificationConfig setTaskPushNotificationConfiguration(TaskPushN
280283
return ProtoUtils.FromProto.taskPushNotificationConfig(responseBuilder);
281284
} catch (A2AClientException e) {
282285
throw e;
283-
} catch (IOException | InterruptedException e) {
286+
} catch (IOException | InterruptedException | JsonProcessingException e) {
284287
throw new A2AClientException("Failed to set task push notification config: " + e, e);
285288
}
286289
}
@@ -395,6 +398,8 @@ public void resubscribe(TaskIdParams request, Consumer<StreamingEventKind> event
395398
throw new A2AClientException("Failed to send streaming message request: " + e, e);
396399
} catch (InterruptedException e) {
397400
throw new A2AClientException("Send streaming message request timed out: " + e, e);
401+
} catch (JsonProcessingException e) {
402+
throw new A2AClientException("Failed to process JSON for streaming message request: " + e, e);
398403
}
399404
}
400405

@@ -424,10 +429,10 @@ public AgentCard getAgentCard(@Nullable ClientCallContext context) throws A2ACli
424429
throw RestErrorMapper.mapRestError(response);
425430
}
426431
String httpResponseBody = response.body();
427-
agentCard = Utils.OBJECT_MAPPER.readValue(httpResponseBody, AgentCard.class);
432+
agentCard = JsonUtil.fromJson(httpResponseBody, AgentCard.class);
428433
needsExtendedCard = false;
429434
return agentCard;
430-
} catch (IOException | InterruptedException e) {
435+
} catch (IOException | InterruptedException | JsonProcessingException e) {
431436
throw new A2AClientException("Failed to get authenticated extended agent card: " + e, e);
432437
} catch (A2AClientError e) {
433438
throw new A2AClientException("Failed to get agent card: " + e, e);
@@ -451,7 +456,7 @@ private PayloadAndHeaders applyInterceptors(String methodName, @Nullable Message
451456
return payloadAndHeaders;
452457
}
453458

454-
private String sendPostRequest(String url, PayloadAndHeaders payloadAndHeaders) throws IOException, InterruptedException {
459+
private String sendPostRequest(String url, PayloadAndHeaders payloadAndHeaders) throws IOException, InterruptedException, JsonProcessingException {
455460
A2AHttpClient.PostBuilder builder = createPostBuilder(url, payloadAndHeaders);
456461
A2AHttpResponse response = builder.post();
457462
if (!response.success()) {

examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldClient.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import java.util.function.BiConsumer;
99
import java.util.function.Consumer;
1010

11-
import com.fasterxml.jackson.databind.ObjectMapper;
1211
import io.a2a.A2A;
1312

1413
import io.a2a.client.Client;
@@ -17,6 +16,7 @@
1716
import io.a2a.client.http.A2ACardResolver;
1817
import io.a2a.client.transport.jsonrpc.JSONRPCTransport;
1918
import io.a2a.client.transport.jsonrpc.JSONRPCTransportConfig;
19+
import io.a2a.json.JsonUtil;
2020
import io.a2a.spec.AgentCard;
2121
import io.a2a.spec.Message;
2222
import io.a2a.spec.Part;
@@ -30,14 +30,13 @@ public class HelloWorldClient {
3030

3131
private static final String SERVER_URL = "http://localhost:9999";
3232
private static final String MESSAGE_TEXT = "how much is 10 USD in INR?";
33-
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
3433

3534
public static void main(String[] args) {
3635
try {
3736
AgentCard finalAgentCard = null;
3837
AgentCard publicAgentCard = new A2ACardResolver("http://localhost:9999").getAgentCard();
3938
System.out.println("Successfully fetched public agent card:");
40-
System.out.println(OBJECT_MAPPER.writeValueAsString(publicAgentCard));
39+
System.out.println(JsonUtil.toJson(publicAgentCard));
4140
System.out.println("Using public agent card for client initialization (default).");
4241
finalAgentCard = publicAgentCard;
4342

@@ -47,7 +46,7 @@ public static void main(String[] args) {
4746
authHeaders.put("Authorization", "Bearer dummy-token-for-extended-card");
4847
AgentCard extendedAgentCard = A2A.getAgentCard(SERVER_URL, "/agent/authenticatedExtendedCard", authHeaders);
4948
System.out.println("Successfully fetched authenticated extended agent card:");
50-
System.out.println(OBJECT_MAPPER.writeValueAsString(extendedAgentCard));
49+
System.out.println(JsonUtil.toJson(extendedAgentCard));
5150
System.out.println("Using AUTHENTICATED EXTENDED agent card for client initialization.");
5251
finalAgentCard = extendedAgentCard;
5352
} else {

extras/push-notification-config-store-database-jpa/src/main/java/io/a2a/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStore.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import jakarta.persistence.PersistenceContext;
1010
import jakarta.transaction.Transactional;
1111

12-
import com.fasterxml.jackson.core.JsonProcessingException;
12+
import io.a2a.json.JsonProcessingException;
1313
import io.a2a.server.tasks.PushNotificationConfigStore;
1414
import io.a2a.spec.PushNotificationConfig;
1515
import org.slf4j.Logger;

extras/push-notification-config-store-database-jpa/src/main/java/io/a2a/extras/pushnotificationconfigstore/database/jpa/JpaPushNotificationConfig.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
import jakarta.persistence.Table;
77
import jakarta.persistence.Transient;
88

9-
import com.fasterxml.jackson.core.JsonProcessingException;
9+
import io.a2a.json.JsonProcessingException;
10+
import io.a2a.json.JsonUtil;
1011
import io.a2a.spec.PushNotificationConfig;
11-
import io.a2a.util.Utils;
1212

1313
@Entity
1414
@Table(name = "a2a_push_notification_configs")
@@ -46,7 +46,7 @@ public void setConfigJson(String configJson) {
4646

4747
public PushNotificationConfig getConfig() throws JsonProcessingException {
4848
if (config == null) {
49-
this.config = Utils.unmarshalFrom(configJson, PushNotificationConfig.TYPE_REFERENCE);
49+
this.config = JsonUtil.fromJson(configJson, PushNotificationConfig.class);
5050
}
5151
return config;
5252
}
@@ -56,12 +56,12 @@ public void setConfig(PushNotificationConfig config) throws JsonProcessingExcept
5656
throw new IllegalArgumentException("Mismatched config id. " +
5757
"Expected '" + id.getConfigId() + "'. Got: '" + config.id() + "'");
5858
}
59-
configJson = Utils.OBJECT_MAPPER.writeValueAsString(config);
59+
configJson = JsonUtil.toJson(config);
6060
this.config = config;
6161
}
6262

6363
static JpaPushNotificationConfig createFromConfig(String taskId, PushNotificationConfig config) throws JsonProcessingException {
64-
String json = Utils.OBJECT_MAPPER.writeValueAsString(config);
64+
String json = JsonUtil.toJson(config);
6565
JpaPushNotificationConfig jpaPushNotificationConfig =
6666
new JpaPushNotificationConfig(new TaskConfigId(taskId, config.id()), json);
6767
jpaPushNotificationConfig.config = config;

0 commit comments

Comments
 (0)