FasterXML/jackson-module-parameter-names

2.7.1 with parameter-names module fails to deserialize into map

cowtowncoder opened this issue · 12 comments

(note: moved from FasterXML/jackson-databind#1141)

I'm having a model in a service that heavily depends on custom serializations. Everything is fine and dandy with that. But - when I tested the service (spring boot) I noticed that the actuator endpoint failed with NPE (message: "Can not pass null fieldName")

Test showcasing the problem:
https://github.com/jebl01/jackson-test

and specific test code:

    @Test
    public void failsWithModule() throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new ParameterNamesModule(JsonCreator.Mode.PROPERTIES));

        String data = "{\"key1\": \"value1\", \"key2\": \"value2\"}";

        Map<String, String> result = mapper.readValue(data, HashMap.class);
        result.forEach((key, value) -> System.out.println(key + " : " + value));
    }

and stack trace

com.fasterxml.jackson.databind.JsonMappingException: Instantiation of [map type; class java.util.HashMap, [simple type, class java.lang.Object] -> [simple type, class java.lang.Object]] value failed (java.lang.NullPointerException): null
 at [Source: {"key1": "value1", "key2": "value2"}; line: 1, column: 36] (through reference chain: java.util.HashMap["N/A"])
    at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:223)
    at com.fasterxml.jackson.databind.deser.std.StdValueInstantiator.wrapAsJsonMappingException(StdValueInstantiator.java:445)
    at com.fasterxml.jackson.databind.deser.std.StdValueInstantiator.rewrapCtorProblem(StdValueInstantiator.java:464)
    at com.fasterxml.jackson.databind.deser.std.StdValueInstantiator.createFromObjectWith(StdValueInstantiator.java:258)
    at com.fasterxml.jackson.databind.deser.impl.PropertyBasedCreator.build(PropertyBasedCreator.java:135)
    at com.fasterxml.jackson.databind.deser.std.MapDeserializer._deserializeUsingCreator(MapDeserializer.java:578)
    at com.fasterxml.jackson.databind.deser.std.MapDeserializer.deserialize(MapDeserializer.java:320)
    at com.fasterxml.jackson.databind.deser.std.MapDeserializer.deserialize(MapDeserializer.java:1)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3789)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2779)
    at jebl01.ParameterNamesModuleTest.failsWithModule(ParameterNamesModuleTest.java:37)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:483)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:675)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)
Caused by: java.lang.NullPointerException
    at java.util.HashMap.putMapEntries(HashMap.java:500)
    at java.util.HashMap.<init>(HashMap.java:489)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:408)
    at com.fasterxml.jackson.databind.introspect.AnnotatedConstructor.call(AnnotatedConstructor.java:114)
    at com.fasterxml.jackson.databind.deser.std.StdValueInstantiator.createFromObjectWith(StdValueInstantiator.java:256)
    ... 30 more

Some notes:

This problem is due to the feature that allows auto-detection of public constructors even without @JsonCreator annotation, under certain conditions. Since such auto-detection is inherently risky, I am not sure whether this problem can be resolved reliably; constructor in question is public, and takes single Map argument (with detected name of m or such).

@lpandzic any ideas here?

From my understanding, the problem is that JsonCreator.Mode.PROPERTIES is built atop of a assumption that constructor parameters should exactly match the properties and regular Java API classes contain a lot of utility constructors in them which don't match this assumption.
There exist use cases for both single parameter delegation creators and single parameter property creators.

Was JsonCreator.Mode.DEFAULT and delegating creator behavior in jackson designed only to match those utility constructors that are present in Java API classes and which don't have default constructors?

One more thing, why does
Map<String, String> result = mapper.readValue(data, HashMap.class); fail and
Map<String, String> result = mapper.readValue(data, new TypeReference<Map<String, String>>() {}); pass?

@lpandzic PROPERTIES does not assume that these are the only properties that exist; there may well be other properties. But even if that was limited, there is no way for it to know what kind of content JSON might have -- deserializer construction has no access to any particular payload, and even if it did, that would be just one of many. So there is no way to try to determine compatibility to actual data coming in, all information there is comes from Java class definition.

Mode.DEFAULT is roughly similar as not specifying mode, which will then default to older behavior (1-arg one defaulting to DELEGATING).

Whether there exists a no-arguments constructor does not change behavior currenty.

As to HashMap vs TypeReference<Map<String,String>>, good question. I do not answer to this.
It is possible there are some flaws in handling there, as it would seem like they should work similarly.
My guess is that in latter case constructors are not introspected for some reason.

@jebl01 One more idea: you may want to sub-class ParameterNamesAnnotationIntrospector and mask method that introspecotr JsonCreator.Mode, to return null for case of HashMap (and other problem cases). I know this is not optimal solution, but perhaps adequate workaround on short-term.

@cowtowncoder ok, will try!

Note! I when I tried TypeReference<Map<String,String>> in my "production code", that failed as well. So, even if it works in this isolated test-case, there is something fishy with that to.

@jebl01 One more idea: you may want to sub-class ParameterNamesAnnotationIntrospector and mask method that introspecotr JsonCreator.Mode, to return null for case of HashMap (and other problem cases). I know this is not optimal solution, but perhaps adequate workaround on short-term.

It already does return null for all the Java API classes. JDK isn't compiled with -parameters.

@lpandzic "I feel illuminated..." you have to up your emoji skills ;-)
https://www.quora.com/Why-do-we-use-a-light-bulb-to-illustrate-an-idea

I know what it means, but wanted an elaboration :)

Not sure if and how to approach this. There are issues for jackson-databind, for rewriting creator introspection (there are some problems with late detection of creator properties), as well as allowing better auto-detection. So hoping those will cover this.