Search before asking

  • [x] I searched in the issues and found nothing similar.

Describe the bug

Continuation of https://github.com/FasterXML/jackson-databind/issues/4922.

Version Information

2.19.1, 2.19.2, 2.18.3

Reproduction

When the containing class of the Map is constructed with a @JsonCreator constructor, the issue comes back.

import java.util.HashMap;
import java.util.Map;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonMerge;
import com.fasterxml.jackson.core.JsonParser.Feature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;

interface MyMap<K, V> extends Map<K, V> {}

class MapImpl<K, V> extends HashMap<K, V> implements MyMap<K, V> {}

class MapUtils {

    static <K, V> MyMap<K, V> createMyMap() {
        return new MapImpl<K, V>();
    }
}

class MergeMap {

    @JsonCreator
    MergeMap(int inter, String s) {
        System.out.println("creator for " + map.getClass().getSimpleName());
        this.inter = inter;
        this.s = s;
    }

    int inter;
    String s;

    public int getInter() {
        return inter;
    }

//  public String getS() {
//      return s;
//  }

    @JsonMerge
    MyMap<Integer, String> map = MapUtils.createMyMap();

    public MyMap<Integer, String> getMap() {
        System.out.println("getMap");
        return map;
    }

    @Override
    public String toString() {
        return map.toString() + " " + inter + " " + s;
    }

    public static void main(String[] args) throws Exception {
        JsonMapper MAPPER = JsonMapper.builder()
                .addModule(new ParameterNamesModule())
                .configure(Feature.INCLUDE_SOURCE_IN_LOCATION, true)
                .build();

        var merge = new MergeMap(5, "f");
        merge.getMap().put(3, "ADS");
        System.out.println(merge);

        System.out.println(" == serializing --");

        var string = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(merge);
        System.out.println(string);

        System.out.println(" == deserializing --");

        var merge2 = MAPPER.readValue(string, MergeMap.class);

        System.out.println(" == checking --");

        System.out.println(merge2);
    }
}

Gives

creator for MapImpl
getMap
{3=ADS} 5 f
 == serializing --
getMap
{
  "inter" : 5,
  "map" : {
    "3" : "ADS"
  }
}
 == deserializing --
Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `example.MyMap` (no Creators, like default constructor, exist): no default constructor found
 at [Source: (String)"{
  "inter" : 5,
  "map" : {
    "3" : "ADS"
  }
}"; line: 3, column: 11] (through reference chain: example.MergeMap["map"])
    at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
    at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1915)
    at com.fasterxml.jackson.databind.DatabindContext.reportBadDefinition(DatabindContext.java:415)
    at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1402)
    at com.fasterxml.jackson.databind.deser.std.MapDeserializer.deserialize(MapDeserializer.java:440)
    at com.fasterxml.jackson.databind.deser.std.MapDeserializer.deserialize(MapDeserializer.java:31)
    at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:543)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeWithErrorWrapping(BeanDeserializer.java:587)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:480)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1499)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:340)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:177)
    at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:342)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4971)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3887)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3855)
    at example.MergeMap.main(MergeMap.java:74)

Uncommenting the getS() method resolves the issue, but shouldn't have affected the construction of the map. Also, declaring Map<Integer, String> map = MapUtils.createMyMap(); instead also works, but the whole point is to merge to an initialized custom type. It shows that the containing class construction is valid, however.

Expected behavior

No response

Additional context

No response

Comment From: cowtowncoder

Interestingly enough, dropping second Constructor argument (String s) also makes test pass. Very strange.

Comment From: cowtowncoder

I suspect there's some code path for "incomplete" set of Creator parameters that is wrong... Because also passing JSON with value for "s" makes test pass.

Comment From: nlisker

Then the problem probably exists for custom Collection types as well even with https://github.com/FasterXML/jackson-databind/issues/4783 fixed.

Comment From: cowtowncoder

@nlisker Yes, most likely. Technically this is quite different root cause, but from user POV similar. On plus side Collection and Map type fix here is probably same, if it's due to buffering.

Comment From: cowtowncoder

Yikes. Thinking this through, I realized the problem. The problem is this: MergeMap instance does not exist until its Constructor is called, so map property cannot be accessed before that. JSON content gets buffered and... I am not sure how all state handling goes then.

Will need to tackle this later on, right now I am bit stuck.

Comment From: cowtowncoder

Update: yes, buffering is problematic: only in case of every CreatorProperty being found before anything to merge are things ok. Otherwise to-merge property, "map" needs to be buffered. But by default we actually try deserializing all such properties (instead of buffering raw token stream for later deserialization) which for to-merge-properties calls wrong method as we do not yet have thing to merge with.

What would be needed is different buffering of content stream and deferred deserialization. Will need to see how feasible that is.

Comment From: nlisker

Will test on the next release.

Comment From: cowtowncoder

Turned out neatly solvable -- and even lead to surprise fix for #2692 !