From 92fc4433f75ab2ebb74159e7beb77aa009903dc1 Mon Sep 17 00:00:00 2001 From: Dave Renouf Date: Tue, 21 Jul 2026 09:51:02 -0400 Subject: [PATCH 1/2] fix: parse fractional-second Durations from the Live API CustomDurationDeserializer used Long.parseLong, rejecting the fractional seconds proto3 JSON encodes google.protobuf.Duration with (e.g. "7.280s"). The Live (BidiGenerateContent) server streams these, so the whole message failed to deserialize and voice turns were lost. Parse with BigDecimal to keep full nanosecond precision and sign. Fixes #1120 --- src/main/java/com/google/genai/JsonSerializable.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) 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, From 8337fe813e554e27254f91563331cf89df003afd Mon Sep 17 00:00:00 2001 From: Dave Renouf Date: Tue, 21 Jul 2026 09:51:02 -0400 Subject: [PATCH 2/2] test: cover fractional-second Duration parsing Unit tests for CustomDurationDeserializer via JsonSerializable.fromJsonString: whole seconds, proto3 fractional seconds ("7.280s"), nanosecond precision, negative fractional values, and rejection of non-numeric input. --- .../google/genai/JsonSerializableTest.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) 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); + }); + } }