diff --git a/src/main/java/com/google/genai/JsonSerializable.java b/src/main/java/com/google/genai/JsonSerializable.java index 52d1db05f4e..9f922580f27 100644 --- a/src/main/java/com/google/genai/JsonSerializable.java +++ b/src/main/java/com/google/genai/JsonSerializable.java @@ -78,8 +78,15 @@ public java.time.Duration deserialize(JsonParser p, DeserializationContext ctxt) if (value.endsWith("s")) { String secondsPart = value.substring(0, value.length() - 1); try { - long seconds = Long.parseLong(secondsPart); - return java.time.Duration.ofSeconds(seconds); + // proto3 JSON encodes google.protobuf.Duration with FRACTIONAL + // seconds (e.g. "7.280s", up to 9 fractional digits), which the Live + // API streams. BigDecimal keeps full nanosecond precision and sign + // without floating-point error; plain integer parsing rejected them. + java.math.BigDecimal seconds = new java.math.BigDecimal(secondsPart); + long wholeSeconds = seconds.longValue(); + long nanoAdjustment = + seconds.subtract(java.math.BigDecimal.valueOf(wholeSeconds)).movePointRight(9).longValue(); + return java.time.Duration.ofSeconds(wholeSeconds, nanoAdjustment); } catch (NumberFormatException e) { throw ctxt.weirdStringException( value, diff --git a/src/test/java/com/google/genai/JsonSerializableTest.java b/src/test/java/com/google/genai/JsonSerializableTest.java index f017b219eec..3059b79fa6f 100644 --- a/src/test/java/com/google/genai/JsonSerializableTest.java +++ b/src/test/java/com/google/genai/JsonSerializableTest.java @@ -24,6 +24,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import com.google.genai.errors.GenAiIOException; +import java.time.Duration; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -131,4 +132,53 @@ void setMaxReadLength_withInvalidInput_throwsException() { }); assertEquals("Invalid JSON max read length: -100", ex2.getMessage()); } + + /** A helper class with a Duration field, exercising CustomDurationDeserializer. */ + static class DurationPayload extends JsonSerializable { + @JsonProperty("timeout") + public Duration timeout; + } + + @Test + void fromJsonString_duration_parsesWholeSeconds() { + DurationPayload payload = + DurationPayload.fromJsonString("{\"timeout\":\"7s\"}", DurationPayload.class); + + assertEquals(Duration.ofSeconds(7), payload.timeout); + } + + @Test + void fromJsonString_duration_parsesFractionalSeconds() { + // proto3 JSON encodes google.protobuf.Duration with fractional seconds + // (e.g. "7.280s", up to 9 fractional digits); the Live API streams these. + DurationPayload payload = + DurationPayload.fromJsonString("{\"timeout\":\"7.280s\"}", DurationPayload.class); + + assertEquals(Duration.ofSeconds(7, 280_000_000L), payload.timeout); + } + + @Test + void fromJsonString_duration_keepsNanosecondPrecision() { + DurationPayload payload = + DurationPayload.fromJsonString("{\"timeout\":\"0.000000001s\"}", DurationPayload.class); + + assertEquals(Duration.ofNanos(1), payload.timeout); + } + + @Test + void fromJsonString_duration_parsesNegativeFractionalSeconds() { + DurationPayload payload = + DurationPayload.fromJsonString("{\"timeout\":\"-1.5s\"}", DurationPayload.class); + + assertEquals(Duration.ofMillis(-1500), payload.timeout); + } + + @Test + void fromJsonString_duration_rejectsNonNumericValue() { + assertThrows( + GenAiIOException.class, + () -> { + DurationPayload.fromJsonString("{\"timeout\":\"abcs\"}", DurationPayload.class); + }); + } }