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); + } }