FasterXML / jackson-modules-java8

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

Unable to deserialize `YearMonth` when running as java9 module, added with `@JsonDeserialize` annotation #202

Closed walkeros closed 3 years ago

walkeros commented 3 years ago

Consider following field annotated with YearMonth deserializer:


class MyClass {
  @JsonSerialize(using = YearMonthSerializer.class)
  @JsonDeserialize(using = YearMonthDeserializer.class)
  private final YearMonth period;
}

It looks to impossible to deserialize in such a way when running your app as named (java9) module. You will get:

java.lang.reflect.InaccessibleObjectException: Unable to make private com.fasterxml.jackson.datatype.jsr310.deser.YearMonthDeserializer() accessible: module com.fasterxml.jackson.datatype.jsr310 does not "opens com.fasterxml.jackson.datatype.jsr310.deser" to module com.fasterxml.jackson.databind
        at java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:340) ~[?:?]
        at java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:280) ~[?:?]
        at java.lang.reflect.Constructor.checkCanSetAccessible(Constructor.java:189) ~[?:?]
        at java.lang.reflect.Constructor.setAccessible(Constructor.java:182) ~[?:?]
        at com.fasterxml.jackson.databind.util.ClassUtil.checkAndFixAccess(ClassUtil.java:934) ~[jackson-databind-2.11.4.jar:?]
        at com.fasterxml.jackson.databind.util.ClassUtil.findConstructor(ClassUtil.java:568) ~[jackson-databind-2.11.4.jar:?]
        at com.fasterxml.jackson.databind.util.ClassUtil.createInstance(ClassUtil.java:550) ~[jackson-databind-2.11.4.jar:?]
        at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.deserializerInstance(DefaultDeserializationContext.java:229) ~[jackson-databind-2.11.4.jar:?]
        at com.fasterxml.jackson.databind.deser.BasicDeserializerFactory.findDeserializerFromAnnotation(BasicDeserializerFactory.java:2099) ~[jackson-databind-2.11.4.jar:?]
        at com.fasterxml.jackson.databind.deser.BasicDeserializerFactory.constructCreatorProperty(BasicDeserializerFactory.java:1019) ~[jackson-databind-2.11.4.jar:?]
        at com.fasterxml.jackson.databind.deser.BasicDeserializerFactory._addExplicitPropertyCreator(BasicDeserializerFactory.java:634) ~[jackson-databind-2.11.4.jar:?]
        at com.fasterxml.jackson.databind.deser.BasicDeserializerFactory._addExplicitAnyCreator(BasicDeserializerFactory.java:661) ~[jackson-databind-2.11.4.jar:?]
        at com.fasterxml.jackson.databind.deser.BasicDeserializerFactory._addDeserializerConstructors(BasicDeserializerFactory.java:411) ~[jackson-databind-2.11.4.jar:?]
        at com.fasterxml.jackson.databind.deser.BasicDeserializerFactory._constructDefaultValueInstantiator(BasicDeserializerFactory.java:283) ~[jackson-databind-2.11.4.jar:?]
        at com.fasterxml.jackson.databind.deser.BasicDeserializerFactory.findValueInstantiator(BasicDeserializerFactory.java:224) ~[jackson-databind-2.11.4.jar:?]
        at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.buildBeanDeserializer(BeanDeserializerFactory.java:220) ~[jackson-databind-2.11.4.jar:?]
        at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.createBeanDeserializer(BeanDeserializerFactory.java:143) ~[jackson-databind-2.11.4.jar:?]
        at com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer2(DeserializerCache.java:414) ~[jackson-databind-2.11.4.jar:?]
        at com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer(DeserializerCache.java:349) ~[jackson-databind-2.11.4.jar:?]
        at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:264) ~[jackson-databind-2.11.4.jar:?]
        at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCacheValueDeserializer(DeserializerCache.java:244) ~[jackson-databind-2.11.4.jar:?]
        at com.fasterxml.jackson.databind.deser.DeserializerCache.findValueDeserializer(DeserializerCache.java:142) ~[jackson-databind-2.11.4.jar:?]
        at com.fasterxml.jackson.databind.DeserializationContext.findContextualValueDeserializer(DeserializationContext.java:458) ~[jackson-databind-2.11.4.jar:?]
        at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.createContextual(CollectionDeserializer.java:181) ~[jackson-databind-2.11.4.jar:?]
        at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.createContextual(CollectionDeserializer.java:26) ~[jackson-databind-2.11.4.jar:?]
        at com.fasterxml.jackson.databind.DeserializationContext.handlePrimaryContextualization(DeserializationContext.java:665) ~[jackson-databind-2.11.4.jar:?]
        at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.resolve(BeanDeserializerBase.java:508) ~[jackson-databind-2.11.4.jar:?]
        at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:293) ~[jackson-databind-2.11.4.jar:?]
        at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCacheValueDeserializer(DeserializerCache.java:244) ~[jackson-databind-2.11.4.jar:?]
        at com.fasterxml.jackson.databind.deser.DeserializerCache.findValueDeserializer(DeserializerCache.java:142) ~[jackson-databind-2.11.4.jar:?]
        at com.fasterxml.jackson.databind.DeserializationContext.findRootValueDeserializer(DeserializationContext.java:491) ~[jackson-databind-2.11.4.jar:?]
        at com.fasterxml.jackson.databind.ObjectMapper._findRootDeserializer(ObjectMapper.java:4713) ~[jackson-databind-2.11.4.jar:?]
        at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4522) ~[jackson-databind-2.11.4.jar:?]
        at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3333) ~[jackson-databind-2.11.4.jar:?]
        at a.b.c.JsonLoader.load(JsonLoader.java:32) ~[abc-1.11.15-SNAPSHOT.jar:?]

The reason for the exception seems is that YearMonthDeserializer is located in jackson-datatype-jsr310 module and has private default constructor. There is an attempt to access that constructor by ClassUtil.checkAndFixAccess which is located in jackson-databind module. Since jackson-datatype-jsr310 does not opens that for reflection the call fails and desrialization is not possible.

Possible solutions would be change the access to constructor from private to public or open that class for reflection to jackson-databind

cowtowncoder commented 3 years ago

Which version is this tested with, 2.12.1?

First: (de)serializers in most modules are intended to be used through registration, and not annotations: so your usage pattern is bit off-label. I know it is done, but I am not quite sure why. So if possible, I would strongly recommend registering the module and not using annotation-based approach.

Having said that, it'd be nice to also make this use case work if possible (there are modules that construct (de)serializers in a way that 0-arg constructor cannot work, but I don't think this particular deserializer is like that).

I'll add this on my todo list to see if simple construct accessibility change would be possible; and if not, opening up of package might be (I think JPMS requires opens for reflection).

walkeros commented 3 years ago

Thanks for having a look.

I encountered this with 2.11.4, but also tested against 2.12.1. The issue is still there as the access to constructor is prohibited.

Answering to "I know it is done, but I am not quite sure why.". In most cases the answer is simplicity. One annotation and it's done, but there is more. Consider that this way you could have different deserializers for the same type (even in single class):


class MyClass {
  @JsonSerialize(using = YearMonthSerializer.class)
  @JsonDeserialize(using = YearMonthDeserializer.class)
  private final YearMonth period;

  @JsonSerialize(using = MyCustomMonthSerializer.class)
  @JsonDeserialize(using = MyCustomMonthDeserializer.class)
  private final YearMonth anotherField;

}

I did not dig into the topic, but AFAIK I could reigister only one deserializer per type, so I would need to have the same for both fields and all the classes, which is not always what is really wanted.

You can also have a look at other classes in the same package. I have not tested them (but went thorugh the code) and saw private constructors for at least one of them as well, so this might affect serializers and deserializers for other types.

cowtowncoder commented 3 years ago

Makes sense. I mostly warn about this being off-brand usage wrt original design as this issue tends to come up from time to time. Will need to think of how to test this as well; main Jackson components will only now move to Java 8 baseline for Jackson 2.13, but there are couple of integration test projects that can use profiles to use later JVM/JDK and those could definitely test that usage works.

cowtowncoder commented 3 years ago

Ok, so, I made the specific change as well as some related ones. I think part of the issue is actually missing "opens" statements in module-info.class, so added what I think should work. Did not add public no-args constructor for all (de)serializers but changed all existing private ones to at least protected: would be great if you could verify that 2.12.0-SNAPSHOT (from Sonatype OSS repo, or local build) works? Would be easy enough to make other changes if you encounter more after getting past initial one(s).

walkeros commented 3 years ago

Ok, let me see this week. I'll test with SNAPSHOT

cowtowncoder commented 3 years ago

@walkeros much appreciated, looking forward to hearing the results!