FasterXML / jackson-modules-java8

Set of support modules for Java 8 datatypes (Optionals, date/time) and features (parameter names)
Apache License 2.0
398 stars 116 forks source link

Jacskon can’t serialize Duration with Long.MIN_VALUE #282

Closed fbacchella closed 9 months ago

fbacchella commented 10 months ago

Java’s JFR can generate evens that return value with a second port of value -9223372036854775808 (equals to Long.MIN_VALUE). Jackson failes to serialize that value:

    @Test
    public void durationBug() throws JsonProcessingException {
        Duration d = Duration.ofSeconds(Long.MIN_VALUE);
        JsonMapper.Builder builder = JsonMapper.builder();
        builder.addModule(new JavaTimeModule());
        builder.configure(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, true);
        ObjectWriter writer = builder.build().writer();
        System.err.println(writer.writeValueAsString(d));
    }

throws:

com.fasterxml.jackson.databind.JsonMappingException: Exceeds capacity of Duration: 9223372036854775808000000000
    at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider._wrapAsIOE(DefaultSerializerProvider.java:508)
    at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider._serialize(DefaultSerializerProvider.java:481)
    at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:318)
    at com.fasterxml.jackson.databind.ObjectWriter$Prefetch.serialize(ObjectWriter.java:1572)
    at com.fasterxml.jackson.databind.ObjectWriter._writeValueAndClose(ObjectWriter.java:1273)
    at com.fasterxml.jackson.databind.ObjectWriter.writeValueAsString(ObjectWriter.java:1140)
    at loghub.receivers.TestJfr.durationBug(TestJfr.java:141)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
    at java.base/java.lang.reflect.Method.invoke(Method.java:578)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
    at org.junit.rules.ExternalResource$1.evaluate(ExternalResource.java:54)
    at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
    at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
    at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
    at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
    at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
    at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:232)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:55)
Caused by: java.lang.ArithmeticException: Exceeds capacity of Duration: 9223372036854775808000000000
    at java.base/java.time.Duration.create(Duration.java:1054)
    at java.base/java.time.Duration.multipliedBy(Duration.java:993)
    at java.base/java.time.Duration.negated(Duration.java:1072)
    at java.base/java.time.Duration.abs(Duration.java:1087)
    at com.fasterxml.jackson.datatype.jsr310.ser.DurationSerializer._toNanos(DurationSerializer.java:144)
    at com.fasterxml.jackson.datatype.jsr310.ser.DurationSerializer.serialize(DurationSerializer.java:129)
    at com.fasterxml.jackson.datatype.jsr310.ser.DurationSerializer.serialize(DurationSerializer.java:52)
    at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider._serialize(DefaultSerializerProvider.java:479)
    ... 33 more

If WRITE_DURATIONS_AS_TIMESTAMPS is set to false, it generate "PT-2562047788015215H-30M-8S"

Jackson version is 2.15.3.

cowtowncoder commented 10 months ago

From stack trace, it looks like Duration serializer was attempting to serialize value as nanoseconds, and that conversion fails due to overflow.

And looking at SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS that is enabled by default... I don't remember quite why this default was selected.

But maybe this could be fixed somehow; the smallest value is kind of special since it's the value for which there is no positive counterpart.

cowtowncoder commented 9 months ago

Ok, the way to enforce serialization as Seconds is like so:

    // [datetime#282]
    static class Bean282 {
        @JsonFormat(pattern = "SECONDS")
        public Duration duration;

        public Bean282(Duration d) { duration = d; }
    }

that is, @JsonFormat is to be used. It is also possible to default this via:

        mapper.configOverride(Duration.class)
            .setFormat(JsonFormat.Value.forPattern("SECONDS"));

I will try to add bit more testing on this, but this is the way to configure things. Use of features will not be sufficient.

Pattern feature was added via #184.