TruncateToMillisecondsTest.java
package tools.jackson.databind.ext.javatime;
import java.time.*;
import org.junit.jupiter.api.Test;
import tools.jackson.databind.*;
import tools.jackson.databind.cfg.DateTimeFeature;
import tools.jackson.databind.testutil.DatabindTestUtil;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests for {@link DateTimeFeature#TRUNCATE_TO_MSECS_ON_READ} and
* {@link DateTimeFeature#TRUNCATE_TO_MSECS_ON_WRITE} features.
*/
public class TruncateToMillisecondsTest extends DatabindTestUtil
{
private final ObjectMapper MAPPER = newJsonMapper();
private final ObjectMapper MAPPER_TRUNCATE_WRITE = jsonMapperBuilder()
.enable(DateTimeFeature.TRUNCATE_TO_MSECS_ON_WRITE)
.build();
private final ObjectMapper MAPPER_TRUNCATE_READ = jsonMapperBuilder()
.enable(DateTimeFeature.TRUNCATE_TO_MSECS_ON_READ)
.build();
private final ObjectMapper MAPPER_TRUNCATE_BOTH = jsonMapperBuilder()
.enable(DateTimeFeature.TRUNCATE_TO_MSECS_ON_WRITE)
.enable(DateTimeFeature.TRUNCATE_TO_MSECS_ON_READ)
.build();
private final ObjectMapper MAPPER_WRITE_AS_TIMESTAMPS = jsonMapperBuilder()
.enable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS)
.build();
private final ObjectMapper MAPPER_TRUNCATE_WRITE_AS_TIMESTAMPS = jsonMapperBuilder()
.enable(DateTimeFeature.TRUNCATE_TO_MSECS_ON_WRITE)
.enable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS)
.build();
// Serialization tests
@Test
public void testInstantSerializationTruncation() throws Exception {
Instant instant = Instant.parse("2023-01-15T10:30:45.123456789Z");
// Without truncation - preserves full nanoseconds
String json1 = MAPPER.writeValueAsString(instant);
assertTrue(json1.contains("123456789") || json1.contains("45.123456789"),
"Expected nanoseconds in: " + json1);
// With truncation - only milliseconds (zeros out extra nanoseconds)
String json2 = MAPPER_TRUNCATE_WRITE.writeValueAsString(instant);
assertTrue(json2.contains("123") && !json2.contains("456789"),
"Expected truncated value in: " + json2);
}
@Test
public void testLocalDateTimeSerializationTruncation() throws Exception {
LocalDateTime ldt = LocalDateTime.of(2023, 1, 15, 10, 30, 45, 123456789);
// Without truncation
String json1 = MAPPER.writeValueAsString(ldt);
assertTrue(json1.contains("123456789") || json1.contains("45.123456789"),
"Expected nanoseconds in: " + json1);
// With truncation
String json2 = MAPPER_TRUNCATE_WRITE.writeValueAsString(ldt);
assertFalse(json2.contains("456789"), "Expected truncated value in: " + json2);
}
@Test
public void testLocalTimeSerializationTruncation() throws Exception {
LocalTime time = LocalTime.of(10, 30, 45, 123456789);
// Without truncation
String json1 = MAPPER.writeValueAsString(time);
assertTrue(json1.contains("123456789") || json1.contains("45.123456789"),
"Expected nanoseconds in: " + json1);
// With truncation
String json2 = MAPPER_TRUNCATE_WRITE.writeValueAsString(time);
assertFalse(json2.contains("456789"), "Expected truncated value in: " + json2);
}
@Test
public void testDurationSerializationTruncation() throws Exception {
Duration duration = Duration.ofSeconds(123, 456789012);
// Without truncation
String json1 = MAPPER.writeValueAsString(duration);
assertTrue(json1.contains("456789012") || json1.contains(".456789012"),
"Expected nanoseconds in: " + json1);
// With truncation
String json2 = MAPPER_TRUNCATE_WRITE.writeValueAsString(duration);
assertTrue(json2.contains("456") || json2.contains(".456"),
"Expected milliseconds in: " + json2);
assertFalse(json2.contains("789012"), "Expected truncated value in: " + json2);
}
// Deserialization tests
@Test
public void testInstantDeserializationTruncation() throws Exception {
String json = "\"2023-01-15T10:30:45.123456789Z\"";
// Without truncation - preserves full nanoseconds
Instant instant1 = MAPPER.readValue(json, Instant.class);
assertEquals(123456789, instant1.getNano());
// With truncation - only milliseconds
Instant instant2 = MAPPER_TRUNCATE_READ.readValue(json, Instant.class);
assertEquals(123000000, instant2.getNano());
}
@Test
public void testLocalDateTimeDeserializationTruncation() throws Exception {
String json = "\"2023-01-15T10:30:45.123456789\"";
// Without truncation
LocalDateTime ldt1 = MAPPER.readValue(json, LocalDateTime.class);
assertEquals(123456789, ldt1.getNano());
// With truncation
LocalDateTime ldt2 = MAPPER_TRUNCATE_READ.readValue(json, LocalDateTime.class);
assertEquals(123000000, ldt2.getNano());
}
@Test
public void testLocalTimeDeserializationTruncation() throws Exception {
String json = "\"10:30:45.123456789\"";
// Without truncation
LocalTime time1 = MAPPER.readValue(json, LocalTime.class);
assertEquals(123456789, time1.getNano());
// With truncation
LocalTime time2 = MAPPER_TRUNCATE_READ.readValue(json, LocalTime.class);
assertEquals(123000000, time2.getNano());
}
@Test
public void testDurationDeserializationTruncation() throws Exception {
String json = "\"PT123.456789012S\"";
// Without truncation
Duration duration1 = MAPPER.readValue(json, Duration.class);
assertEquals(456789012, duration1.getNano());
// With truncation
Duration duration2 = MAPPER_TRUNCATE_READ.readValue(json, Duration.class);
assertEquals(456000000, duration2.getNano());
}
// Round-trip tests
@Test
public void testRoundTripWithBothFeaturesEnabled() throws Exception {
Instant original = Instant.parse("2023-01-15T10:30:45.123456789Z");
// Serialize with truncation
String json = MAPPER_TRUNCATE_BOTH.writeValueAsString(original);
// Deserialize with truncation
Instant result = MAPPER_TRUNCATE_BOTH.readValue(json, Instant.class);
// Result should have milliseconds only
assertEquals(123000000, result.getNano());
assertEquals(original.getEpochSecond(), result.getEpochSecond());
}
@Test
public void testRoundTripSerializeTruncateDeserializeWithout() throws Exception {
LocalDateTime original = LocalDateTime.of(2023, 1, 15, 10, 30, 45, 123456789);
// Serialize with truncation
String json = MAPPER_TRUNCATE_WRITE.writeValueAsString(original);
// Deserialize without truncation (value is already truncated)
LocalDateTime result = MAPPER.readValue(json, LocalDateTime.class);
// Result should still have milliseconds only (from serialization truncation)
assertEquals(123000000, result.getNano());
}
// Test with numeric timestamp format
@Test
public void testInstantNumericTimestampTruncation() throws Exception {
Instant instant = Instant.parse("2023-01-15T10:30:45.123456789Z");
// Serialize as numeric timestamp with truncation
String json = MAPPER_TRUNCATE_WRITE_AS_TIMESTAMPS.writeValueAsString(instant);
// The numeric value should not contain nanoseconds beyond milliseconds
Instant result = MAPPER.readValue(json, Instant.class);
assertTrue(result.getNano() % 1_000_000 == 0,
"Nanoseconds should be multiple of 1,000,000: " + result.getNano());
}
// Test values already at millisecond precision
@Test
public void testAlreadyTruncatedValueRemains() throws Exception {
// Value already at millisecond precision
Instant instant = Instant.parse("2023-01-15T10:30:45.123Z");
assertEquals(123000000, instant.getNano());
// Truncation should not change it
String json = MAPPER_TRUNCATE_WRITE.writeValueAsString(instant);
Instant result = MAPPER_TRUNCATE_READ.readValue(json, Instant.class);
assertEquals(123000000, result.getNano());
assertEquals(instant, result);
}
// Test edge cases
@Test
public void testZeroNanosecondsRemains() throws Exception {
Instant instant = Instant.parse("2023-01-15T10:30:45Z");
assertEquals(0, instant.getNano());
// Truncation should not change it
String json = MAPPER_TRUNCATE_WRITE.writeValueAsString(instant);
Instant result = MAPPER_TRUNCATE_READ.readValue(json, Instant.class);
assertEquals(0, result.getNano());
assertEquals(instant, result);
}
@Test
public void testNegativeDurationTruncation() throws Exception {
// Create a negative duration: -5 seconds
Duration duration = Duration.ofSeconds(-5);
// Serialize with truncation (should not change since it has no fractional part)
String json = MAPPER_TRUNCATE_WRITE.writeValueAsString(duration);
// Deserialize with truncation
Duration result = MAPPER_TRUNCATE_READ.readValue(json, Duration.class);
// Should preserve the negative duration
assertEquals(-5, result.getSeconds());
assertEquals(0, result.getNano());
// Test with negative duration that has fractional seconds
Duration duration2 = Duration.ofMillis(-5123).plusNanos(456789);
String json2 = MAPPER_TRUNCATE_WRITE.writeValueAsString(duration2);
Duration result2 = MAPPER_TRUNCATE_READ.readValue(json2, Duration.class);
// Should truncate to milliseconds
assertTrue(result2.toNanos() % 1_000_000 == 0,
"Expected millisecond precision for negative duration");
}
// Test independence from READ/WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS
@Test
public void testTruncationIndependentOfNanosecondFeature() throws Exception {
ObjectMapper mapperNanosOff = jsonMapperBuilder()
.disable(DateTimeFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
.enable(DateTimeFeature.TRUNCATE_TO_MSECS_ON_WRITE)
.enable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS)
.build();
Instant instant = Instant.parse("2023-01-15T10:30:45.123456789Z");
// Even with nanoseconds disabled, truncation should still occur
String json = mapperNanosOff.writeValueAsString(instant);
Instant result = MAPPER.readValue(json, Instant.class);
// Result should have only millisecond precision
assertTrue(result.getNano() % 1_000_000 == 0,
"Expected millisecond precision: " + result.getNano());
}
// Test with OffsetDateTime and ZonedDateTime
@Test
public void testOffsetDateTimeTruncation() throws Exception {
OffsetDateTime odt = OffsetDateTime.parse("2023-01-15T10:30:45.123456789+01:00");
// Serialize with truncation
String json = MAPPER_TRUNCATE_WRITE.writeValueAsString(odt);
// Deserialize with truncation
OffsetDateTime result = MAPPER_TRUNCATE_READ.readValue(json, OffsetDateTime.class);
assertEquals(123000000, result.getNano());
}
@Test
public void testZonedDateTimeTruncation() throws Exception {
ZonedDateTime zdt = ZonedDateTime.parse("2023-01-15T10:30:45.123456789+01:00[Europe/Paris]");
// Serialize with truncation
String json = MAPPER_TRUNCATE_WRITE.writeValueAsString(zdt);
// Deserialize with truncation
ZonedDateTime result = MAPPER_TRUNCATE_READ.readValue(json, ZonedDateTime.class);
assertEquals(123000000, result.getNano());
}
}