4

It seems to me Jackson JDK8 Data Type module occasionally ignores Parameter Names module which seems a little bit surprising, given that both demand JDK8 and solve specific-use cases with regards to JDK8.

The issue here is that I could not find a way to make JSON deserialization work without parameter names specified explicitly (which is what Parameter Names module is supposed to be all about). It is also exhibiting this behaviour only when trying to pass in JDK8-specific type (Optional<T>) in container object constructor (i.e. normally, this works and I have tested that). The code is compiled with javac parameter -parameters.

The question is - how to make it work so that I can leverage the Parameter Names module (i.e. don't need to specify annotation+value in the constructor and let it figure out the property name by argument name)?

I may be mistaken and haven't looked at the code under the bonnet so I'd like to hear if there is something I have missed.

Let's consider this simple example.

Version stack (all latest versions as of this writing):

private val jacksonVer = "2.6.1"
private val jacksonCore: ModuleID = "com.fasterxml.jackson.core" % "jackson-core" % jacksonVer withSources() withJavadoc()
private val jacksonDataBind: ModuleID = "com.fasterxml.jackson.core" % "jackson-databind" % jacksonVer withSources() withJavadoc()
private val jacksonAnnotations: ModuleID = "com.fasterxml.jackson.core" % "jackson-annotations" % jacksonVer withSources() withJavadoc()
private val jacksonParamNames: ModuleID = "com.fasterxml.jackson.module" % "jackson-module-parameter-names" % "2.6.2" withSources() withJavadoc()
private val jacksonJdk8DataType: ModuleID = "com.fasterxml.jackson.datatype" % "jackson-datatype-jdk8" % "2.4.3" withSources() withJavadoc()

Container:

private static class SimpleTest {
    @JsonProperty private Optional<String> s1;
    @JsonProperty private Optional<String> s2;
    @JsonProperty private Map<String, String> map;

    private SimpleTest(@JsonProperty("s1") Optional<String> s1, @JsonProperty("s2") Optional<String> s2, @JsonProperty("map") Map<String, String> map) {
        this.s1 = s1;
        this.s2 = s2;
        this.map = map;
    }

    static SimpleTest of(Optional<String> s1, Optional<String> s2, Map<String, String> m) {
        return new SimpleTest(s1, s2, m);
    }
}

Serialization:

@Test
public void testSer() throws JsonProcessingException {
    SimpleTest test = SimpleTest.of(Optional.of("a"), Optional.empty(), Collections.emptyMap());
    System.out.println(JacksonUtil.getMapper().writeValueAsString(test));
}

Deserialization:

@Test
public void testDeser() throws IOException {
    String json = "{\n" +
            "  \"s1\" : \"a\",\n" +
            "  \"map\" : { }\n" +
            "}";
    JacksonUtil.getMapper().readValue(json, SimpleTest.class);
}

Running testSer() with such a container yields:

{
  "s1" : "a",
  "s2" : null,
  "map" : { }
}

Running testDeser() with input such as this

{
  "s1" : "a",
  "map" : { }
}

also works, and yields expected results (s1 has value, s2 is Optional.empty and map is empty) but only if the container constructor is defined as above. I could not get it work in following combinations:
1)

private SimpleTest(Optional<String> s1, Optional<String> s2, Map<String, String> map) {...}

2)

private SimpleTest(@JsonProperty Optional<String> s1, @JsonProperty Optional<String> s2, @JsonProperty Map<String, String> map) {...}

By rights, both should work but they don't - both approaches yield the following stacktrace:

com.fasterxml.jackson.databind.JsonMappingException: No suitable constructor found for type [simple type, class com._3esi.load.bootstrap.ScratchPad$SimpleTest]: can not instantiate from JSON object (missing default constructor or creator, or perhaps need to add/enable type information?)
 at [Source: {
  "s1" : "a",
  "map" : { }
}; line: 2, column: 3]
    at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:148)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1106)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:294)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:131)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3731)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2724)

What am I missing here?

quantum
  • 3,000
  • 5
  • 41
  • 56
  • See additional discussions [here](https://github.com/FasterXML/jackson-module-parameter-names/issues/26) and [here](https://github.com/FasterXML/jackson-datatype-jdk8/issues/16). – quantum Sep 22 '15 at 19:05

2 Answers2

4

I think that this is due to one remaining issue with Jackson 2.6, regarding detection of multi-argument constructors: although parameter names are detected, constructor itself is not retained as a candidate without use of @JsonCreator annotation to mark it. This is something that is hoped to be resolved for 2.7 (and was originally supposed to be fixed for 2.6), but for time being it is necessary.

If you add @JsonCreator to constructor and remove @JsonProperty annotations, things should work as expected.

StaxMan
  • 113,358
  • 34
  • 211
  • 239
  • 1
    Thanks for your reply and effort - your approach works and actually the whole issue might have been a hoax/IDE induced as I've discovered javac parameter inexplicably missing from compile settings. What is more - deserialization requires no annotations whatsoever, it simply works. – quantum Sep 22 '15 at 19:34
3

CP of my answer on Github:

I've tested the following code and the test passes:

public class OptionalTest {

    @Test
    public void shouldDeserialize() throws IOException {

        // given
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(new Jdk8Module());
        objectMapper.registerModule(new ParameterNamesModule());

        // when
        String json = "{\"s1\":\"a\",\"map\":{}}";
        SimpleTest simpleTest = objectMapper.readValue(json, SimpleTest.class);

        then(simpleTest).isEqualToComparingFieldByField(new SimpleTest(Optional.of("a"), Optional.empty(), new HashMap<>()));
    }

    private static class SimpleTest {
        private Optional<String> s1;
        private Optional<String> s2;
        private Map<String, String> map;

        private SimpleTest(Optional<String> s1, Optional<String> s2, Map<String, String> map) {
            this.s1 = s1;
            this.s2 = s2;
            this.map = map;
        }

        static SimpleTest of(Optional<String> s1, Optional<String> s2, Map<String, String> m) {
            return new SimpleTest(s1, s2, m);
        }
    }
}

Note that this was tested against latest state in jackson-parameter-name-modules with all dependencies set to 2.6.2.

Lovro Pandžić
  • 5,920
  • 4
  • 43
  • 51
  • 1
    I can confirm the test passes on my end as well - most likely we were chasing ghosts as this seems like an IDE issue (the `-parameters` javac option had vanished from compiler options). Thanks for your help and time to both of you. – quantum Sep 22 '15 at 19:49
  • 1
    Serialization also works, forgot to include it in code above. :) – Lovro Pandžić Sep 22 '15 at 19:56