From 15a13139426cb765806e1af89b8cb59090ecc79b Mon Sep 17 00:00:00 2001
From: Programmer-RD-AI <79456372+Programmer-RD-AI@users.noreply.github.com>
Date: Sat, 21 Jun 2025 10:01:55 +0530
Subject: [PATCH] feat: add Jackson dependencies and implement structured
response handling in ResponseHandler
---
java-vertexai/google-cloud-vertexai/pom.xml | 13 +++-
.../generativeai/ResponseHandler.java | 60 +++++++++++++++++--
.../generativeai/ResponseHandlerTest.java | 52 ++++++++++++++++
3 files changed, 120 insertions(+), 5 deletions(-)
diff --git a/java-vertexai/google-cloud-vertexai/pom.xml b/java-vertexai/google-cloud-vertexai/pom.xml
index 4b37a8c89bc6..751fb10120ae 100644
--- a/java-vertexai/google-cloud-vertexai/pom.xml
+++ b/java-vertexai/google-cloud-vertexai/pom.xml
@@ -101,7 +101,18 @@
truth
test
-
+
+ com.fasterxml.jackson.core
+ jackson-core
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+
com.google.api.grpc
grpc-google-cloud-vertexai-v1
diff --git a/java-vertexai/google-cloud-vertexai/src/main/java/com/google/cloud/vertexai/generativeai/ResponseHandler.java b/java-vertexai/google-cloud-vertexai/src/main/java/com/google/cloud/vertexai/generativeai/ResponseHandler.java
index 129c80d6b6e1..8cf1b97cd52e 100644
--- a/java-vertexai/google-cloud-vertexai/src/main/java/com/google/cloud/vertexai/generativeai/ResponseHandler.java
+++ b/java-vertexai/google-cloud-vertexai/src/main/java/com/google/cloud/vertexai/generativeai/ResponseHandler.java
@@ -16,6 +16,9 @@
package com.google.cloud.vertexai.generativeai;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.json.JsonMapper;
import com.google.cloud.vertexai.api.Candidate;
import com.google.cloud.vertexai.api.Candidate.FinishReason;
import com.google.cloud.vertexai.api.Citation;
@@ -25,13 +28,22 @@
import com.google.cloud.vertexai.api.GenerateContentResponse;
import com.google.cloud.vertexai.api.Part;
import com.google.common.collect.ImmutableList;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.Message;
+import com.google.protobuf.util.JsonFormat;
+
+import java.io.IOException;
+import java.util.*;
/** Helper class to post-process GenerateContentResponse. */
public class ResponseHandler {
+ private static final ObjectMapper DEFAULT_MAPPER = JsonMapper.builder()
+ .enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)
+ .enable(DeserializationFeature.USE_BIG_INTEGER_FOR_INTS)
+ .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
+ .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
+ .enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS)
+ .build();
/**
* Gets the text message in a GenerateContentResponse.
@@ -53,6 +65,46 @@ public static String getText(GenerateContentResponse response) {
return text;
}
+ /**
+ * Deserialises the text of a {@link GenerateContentResponse} into a strongly-typed value.
+ * Behaviour:
+ * - If {@code clazz} extends {@code com.google.protobuf.Message}, the JSON is merged into a
+ * new protobuf builder via {@link JsonFormat}.
+ * - Otherwise the JSON is read with Jackson. If {@code customMapper} is present its
+ * configuration is used; when empty the library’s default {@code MAPPER} is applied.
+ * @param response the Vertex AI response whose first candidate contains JSON
+ * @param clazz target class (protobuf message or POJO)
+ * @param customMapper optional Jackson {@link JsonMapper} to override the default settings
+ * @param concrete return type
+ * @return an instance of {@code T} populated from the model output
+ * @throws IllegalArgumentException if reflection fails, the JSON is invalid, or the payload
+ * cannot be merged into the protobuf builder
+ */
+ @SuppressWarnings("unchecked")
+ public static T getStructuredResponse(
+ GenerateContentResponse response,
+ Class clazz,
+ Optional customMapper) {
+
+ String text = getText(response);
+ if (com.google.protobuf.Message.class.isAssignableFrom(clazz)) {
+ try {
+ Message.Builder builder =
+ (Message.Builder) clazz.getMethod("newBuilder").invoke(null);
+ JsonFormat.parser().ignoringUnknownFields().merge(text, builder);
+ return (T) builder.build();
+ } catch (ReflectiveOperationException | InvalidProtocolBufferException e) {
+ throw new IllegalArgumentException("Parsing as protobuf failed", e);
+ }
+ }
+ ObjectMapper mapper = customMapper.orElse((JsonMapper) DEFAULT_MAPPER);
+ try {
+ return mapper.readValue(text, clazz);
+ } catch (IOException e) {
+ throw new IllegalArgumentException("JSON parse failed", e);
+ }
+ }
+
/**
* Gets the list of function calls in a GenerateContentResponse.
*
diff --git a/java-vertexai/google-cloud-vertexai/src/test/java/com/google/cloud/vertexai/generativeai/ResponseHandlerTest.java b/java-vertexai/google-cloud-vertexai/src/test/java/com/google/cloud/vertexai/generativeai/ResponseHandlerTest.java
index 80487e7355fe..4c05d9db7b70 100644
--- a/java-vertexai/google-cloud-vertexai/src/test/java/com/google/cloud/vertexai/generativeai/ResponseHandlerTest.java
+++ b/java-vertexai/google-cloud-vertexai/src/test/java/com/google/cloud/vertexai/generativeai/ResponseHandlerTest.java
@@ -20,6 +20,8 @@
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.when;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.json.JsonMapper;
import com.google.cloud.vertexai.api.Candidate;
import com.google.cloud.vertexai.api.Candidate.FinishReason;
import com.google.cloud.vertexai.api.Citation;
@@ -31,6 +33,8 @@
import com.google.common.collect.ImmutableList;
import java.util.Arrays;
import java.util.Iterator;
+import java.util.Optional;
+
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -83,6 +87,29 @@ public final class ResponseHandlerTest {
.addCandidates(CANDIDATE_1)
.addCandidates(CANDIDATE_2)
.build();
+ private static final class ExampleDto {
+ public String name;
+ public int count;
+
+ // Default constructor for Jackson
+ public ExampleDto() {
+ }
+
+ ExampleDto(String name, int count) {
+ this.name = name;
+ this.count = count;
+ }
+ }
+
+ private static final String DTO_JSON = "{\"name\":\"vertex\",\"count\":42}";
+ private static final GenerateContentResponse DTO_RESPONSE =
+ GenerateContentResponse.newBuilder()
+ .addCandidates(
+ Candidate.newBuilder()
+ .setContent(
+ Content.newBuilder()
+ .addParts(Part.newBuilder().setText(DTO_JSON))))
+ .build();
@Rule public final MockitoRule mocksRule = MockitoJUnit.rule();
@@ -166,4 +193,29 @@ public void testAggregateStreamIntoResponse() {
assertThat(response.getCandidates(0).getCitationMetadata().getCitationsList())
.isEqualTo(Arrays.asList(CITATION_1, CITATION_2));
}
+
+ @Test
+ public void testGetStructuredResponseWithCustomMapper() {
+ JsonMapper strictMapper =
+ JsonMapper.builder()
+ .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
+ .build();
+
+ ExampleDto dto =
+ ResponseHandler.getStructuredResponse(
+ DTO_RESPONSE, ExampleDto.class, Optional.of(strictMapper));
+
+ assertThat(dto.name).isEqualTo("vertex");
+ assertThat(dto.count).isEqualTo(42);
+ }
+
+ @Test
+ public void testGetStructuredResponseWithDefaultMapper() {
+ ExampleDto dto =
+ ResponseHandler.getStructuredResponse(
+ DTO_RESPONSE, ExampleDto.class, Optional.empty());
+
+ assertThat(dto.name).isEqualTo("vertex");
+ assertThat(dto.count).isEqualTo(42);
+ }
}