Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom LocalDate deserializer not being used with 2.11, jsr-310 datatype module #177

Closed
jeroenvs opened this issue Jun 8, 2020 · 6 comments

Comments

@jeroenvs
Copy link

jeroenvs commented Jun 8, 2020

Dear Jackson team,

It seems that as of Jackson 2.11 my custom (date) deserializer is no longer being used by the object mapper. I've tried various ways of registering the deserializer but none seem to work. In the previous version (2.10.4) this was not a problem.

Strangely the JSR310 LocalDateDeserializer is being detected and registered automatically from the classpath, skipping the deserializer in my custom module.

Example:

public class CustomLocalDateDeserializerTest {

    @Test
    public void parse_dates_shouldSucceed() throws JsonProcessingException {
        SimpleModule module = new SimpleModule();
        module.addDeserializer(LocalDate.class, new CustomLocalDateDeserializer());

        ObjectMapper objectMapper = JsonMapper.builder()
            .addModule(module)
            .build();

        final String json = "{\"endDate\":1567202400000,\"startDate\":1564610400000}";

        Dates dates = objectMapper.readValue(json, Dates.class);
        assertNotNull(dates);
        assertEquals(LocalDate.of(2019, 8, 1), dates.getStartDate());
        assertEquals(LocalDate.of(2019, 8, 31), dates.getEndDate());
    }

    @Getter
    @Setter
    public static class Dates {

        private LocalDate startDate;
        private LocalDate endDate;

    }

}

@Slf4j
public final class CustomLocalDateDeserializer extends com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer {

    public CustomLocalDateDeserializer() {
        super(DateTimeFormatter.ISO_LOCAL_DATE);
    }

    @Override
    public LocalDate deserialize(JsonParser parser, DeserializationContext context) throws IOException {
        if (parser.hasToken(JsonToken.VALUE_NUMBER_INT)) {
            long value = parser.getLongValue();
            if (!isValidEpochDay(value)) {
                return toLocalDate(value);
            }
        }
        return super.deserialize(parser, context);
    }

    private static boolean isValidEpochDay(long value) {
        try {
            ChronoField.EPOCH_DAY.checkValidValue(value);
        } catch (DateTimeException ex) {
            log.trace("Date value {} is not a valid epoch day value.", value);
            return false;
        }
        return true;
    }

    private static LocalDate toLocalDate(Long millis) {
        Instant instant = Instant.ofEpochMilli(millis);
        LocalDateTime ldt = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
        return LocalDate.of(ldt.getYear(), ldt.getMonth(), ldt.getDayOfMonth());
    }

}
@jeroenvs
Copy link
Author

jeroenvs commented Jun 8, 2020

Running the above code leads to the following exception:

com.fasterxml.jackson.databind.JsonMappingException: Invalid value for EpochDay (valid values -365243219162 - 365241780471): 1567202400000 (through reference chain: nl.uas.change.history.CustomLocalDateDeserializerTest$Dates["endDate"])

	at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:397)
	at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:356)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.wrapAndThrow(BeanDeserializerBase.java:1714)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:295)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:156)
	at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4482)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3434)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3402)
	at nl.uas.change.history.CustomLocalDateDeserializerTest.parse_dates_shouldSucceed(CustomLocalDateDeserializerTest.java:30)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	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.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.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:68)
	at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
	at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: java.time.DateTimeException: Invalid value for EpochDay (valid values -365243219162 - 365241780471): 1567202400000
	at java.base/java.time.temporal.ValueRange.checkValidValue(ValueRange.java:311)
	at java.base/java.time.temporal.ChronoField.checkValidValue(ChronoField.java:717)
	at java.base/java.time.LocalDate.ofEpochDay(LocalDate.java:341)
	at com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer.deserialize(LocalDateDeserializer.java:152)
	at com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer.deserialize(LocalDateDeserializer.java:37)
	at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:129)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:293)
	... 30 more

@cowtowncoder
Copy link
Member

Since support for Java 8 date/time types is provided by separate module, will transfer to its issue tracker.

@cowtowncoder cowtowncoder transferred this issue from FasterXML/jackson-core Jun 8, 2020
@cowtowncoder cowtowncoder changed the title Jackson 2.11 Deserializer not used Custom LocalDate deserializer not being used with 2.11, jsr-310 datatype module Jun 8, 2020
@cowtowncoder
Copy link
Member

First things first: unless you ask jackson-datatype-jsr310 module to be registered, Jackson does not register it: there is no mechanism that would do that.
ObjectMapper does have method findAndRegisterModules() (and findModules()), but that would have to be called by something else.

But you are actually extending deserializer from that package so I don't think this is what happens. You are instead calling super.deserialize() which calls underlying method.

@kupci
Copy link
Member

kupci commented Jun 8, 2020

Realize this doesn't solve your present issue, but as a side note (and note to self), there is some related work started to allow Instant to be serialized as epoch seconds without a fractional part.

@jeroenvs
Copy link
Author

jeroenvs commented Jun 9, 2020

Pardon, I've found the probleem and it's indeed not a bug. Between 2.10.4 en 2.11.0 the deserializer now uses the following method:

    @Override
    protected LocalDateDeserializer withShape(JsonFormat.Shape shape) { return new LocalDateDeserializer(this, shape); }

Which creates a new deserializer, replacing my custom deserializer.
I've solved the problem by using composition over inheritance:

public final class CustomLocalDateDeserializer extends StdDeserializer<LocalDate> {

    protected CustomLocalDateDeserializer() {
        super(LocalDate.class);
    }

    @Override
    public LocalDate deserialize(JsonParser parser, DeserializationContext context) throws IOException {
        if (parser.hasToken(JsonToken.VALUE_NUMBER_INT)) {
            long value = parser.getLongValue();
            if (!isValidEpochDay(value)) {
                return toLocalDate(value);
            }
        }

        return LocalDateDeserializer.INSTANCE.deserialize(parser, context);
    }

    private static boolean isValidEpochDay(long value) {
        try {
            ChronoField.EPOCH_DAY.checkValidValue(value);
        } catch (DateTimeException ex) {
            return false;
        }
        return true;
    }

    private static LocalDate toLocalDate(Long millis) {
        Instant instant = Instant.ofEpochMilli(millis);
        LocalDateTime ldt = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
        return LocalDate.of(ldt.getYear(), ldt.getMonth(), ldt.getDayOfMonth());
    }

}

@jeroenvs jeroenvs closed this as completed Jun 9, 2020
@cowtowncoder
Copy link
Member

Ok, glad you figure out the issue. Sub-classing is quite fragile, unfortunately, and composition is encouraged, for this reason.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants