Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/main/java/com/google/genai/JsonSerializable.java
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
50 changes: 50 additions & 0 deletions src/test/java/com/google/genai/JsonSerializableTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
});
}
}
Loading