Search before asking

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

Describe the bug

If both JsonCreator and DefaultCreator are reported, the JsonCreator should take precedence. On the other hand, in certain cases, the DefaultCreator may take precedence.

This issue is a Javaized version of the following https://github.com/FasterXML/jackson-module-kotlin/issues/932 https://github.com/FasterXML/jackson-databind/issues/5040

Version Information

  • 2.19.0-SNAPSHOT
  • Probably the same for 2.18.3.

Reproduction

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.introspect.AnnotatedClass;
import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector;
import com.fasterxml.jackson.databind.introspect.PotentialCreator;
import com.fasterxml.jackson.databind.json.JsonMapper;
import org.junit.jupiter.api.Test;

import java.util.List;

public class Kotlin932Java {
    static class User {
        private final int age;

        public User(int age) { this.age = age; }

        @JsonCreator
        public User() { this(0); }

        public int getAge() { return age; }
    }

    static class AI extends NopAnnotationIntrospector {
        @Override
        public PotentialCreator findDefaultCreator(MapperConfig<?> config,
                                                   AnnotatedClass valueClass,
                                                   List<PotentialCreator> declaredConstructors,
                                                   List<PotentialCreator> declaredFactories) {
            if (valueClass.getRawType() != User.class) return null;

            return declaredConstructors.stream()
                    .filter(it -> it.paramCount() != 0)
                    .findFirst()
                    .orElse(null);
        }
    }

    @Test
    public void kotlin932() throws JsonProcessingException {
        ObjectMapper mapper = JsonMapper.builder().annotationIntrospector(new AI()).build();
        String json =
                "{\n" +
                "  \"age\": 25\n" +
                "}";

        User user = mapper.readValue(json, User.class);

        System.out.println(user);
    }
}

Expected behavior

Deserialization should succeed, but in fact has no property name ... is reported.

Additional context

The full stack trace when run on the 2.19 branch of kotlin-module.

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Invalid type definition for type `com.fasterxml.jackson.module.kotlin.test.github.Kotlin932Java$User`: Argument #0 of Creator [constructor for `com.fasterxml.jackson.module.kotlin.test.github.Kotlin932Java$User` (1 arg), annotations: [null] has no property name (and is not Injectable): can not use as property-based Creator
 at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 1, column: 1]

    at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:62)
    at com.fasterxml.jackson.databind.DeserializationContext.reportBadTypeDefinition(DeserializationContext.java:1893)
    at com.fasterxml.jackson.databind.deser.BasicDeserializerFactory._addSelectedPropertiesBasedCreator(BasicDeserializerFactory.java:535)
    at com.fasterxml.jackson.databind.deser.BasicDeserializerFactory._constructDefaultValueInstantiator(BasicDeserializerFactory.java:259)
    at com.fasterxml.jackson.databind.deser.BasicDeserializerFactory.findValueInstantiator(BasicDeserializerFactory.java:209)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.buildBeanDeserializer(BeanDeserializerFactory.java:263)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.createBeanDeserializer(BeanDeserializerFactory.java:152)
    at com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer2(DeserializerCache.java:472)
    at com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer(DeserializerCache.java:416)
    at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:318)
    at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCacheValueDeserializer(DeserializerCache.java:285)
    at com.fasterxml.jackson.databind.deser.DeserializerCache.findValueDeserializer(DeserializerCache.java:175)
    at com.fasterxml.jackson.databind.DeserializationContext.findRootValueDeserializer(DeserializationContext.java:669)
    at com.fasterxml.jackson.databind.ObjectMapper._findRootDeserializer(ObjectMapper.java:5102)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4972)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3887)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3855)
    at com.fasterxml.jackson.module.kotlin.test.github.Kotlin932Java.kotlin932(Kotlin932Java.java:50)
    at java.base/java.lang.reflect.Method.invoke(Method.java:569)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)

Comment From: cowtowncoder

Ok. Looks like possible bug, although usage is bit odd -- if 0-args Constructor is expected to be used (I think?), how should age be passed?

Also, instead of

System.out.println(user);

it'd be better to assert (I know, there's now exception instead, but eventually this should work).

I think the problem probably has to do with special handling of "default" (0-args) construct (note: "findDefaultCreator()" is bad name, renamed in 3.0 as "findPreferredCreator". It should be recognized, I suppose, as Properties-based one. But unfortunately there is some specific code for use of default constructors.

ONe quick thing: does addition of mode = Mode.PROPERTIES on @JsonCreator help?

Comment From: k163377

if 0-args Constructor is expected to be used

This is the state of affairs for minimum reproduction. Originally, additional properties were registered from the setter after initialization with a 0-args constructor.

Also, instead of

Oops, I forgot to correct that.

mode = Mode.PROPERTIES on @JsonCreator help?

I tried all valid modes, but unfortunately they did not seem to work.

Comment From: cowtowncoder

Trying to figure this out. One big conceptual/modeling problem is that 0-param "default" constructors (alas, overloaded term) are NOT included in regular declared constructors List, but as separate things. Changing that seems risky, unfortunately. But maybe can get this particular case to work at least... somehow.

Comment From: cowtowncoder

Ok: was able to fix. Note tho that when using 0-param Constructor, "age" has to be passed some other way (or ignored) -- I modified test to use empty Object.

Will probably also file a follow-up ticket for actually passing 0-param constructor to AnnotationIntrospector -- but that will be 3.0-only being API change.