Search before asking

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

Describe the bug

As discussed with @cowtowncoder in https://github.com/spring-projects/spring-boot/issues/46659, there appears to be an increased need for @JsonCreator with Jackson 3.0.0-rc6 compared to 2.19.2.

Version Information

  • 3.0.0-rc6
  • 2.19.2

Reproduction

package com.example;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;

import tools.jackson.databind.json.JsonMapper;

public class Example {

    public static void main(String[] args) throws Exception {
        String json = "{\"productId\":5, \"name\":\"test\", \"weight\":42}";
        JsonMapper jackson3Mapper = tools.jackson.databind.json.JsonMapper.builder().build();
        System.out.println(jackson3Mapper.readValue(json, Pojo.class).getProductId());
        ObjectMapper jackson2Mapper = com.fasterxml.jackson.databind.json.JsonMapper.builder().build();
        jackson2Mapper.registerModule(new ParameterNamesModule());
        System.out.println(jackson2Mapper.readValue(json, Pojo.class).getProductId());
    }

    public static class Pojo {

        private final int productId;

        private final String name;

        private final int weight;

        public Pojo() {
            this.productId = 0;
            this.name = null;
            this.weight = 0;
        }

        public Pojo(int productId, String name, int weight) {
            this.productId = productId;
            this.name = name;
            this.weight = weight;
        }

        public int getProductId() {
            return this.productId;
        }

        public String getName() {
            return this.name;
        }

        public int getWeight() {
            return this.weight;
        }

    }

}

The above should be compiled with -parameters. When run, it should produce the following output:

0
5

Jackson 3 uses the default constructor to create Pojo, Jackson 2 uses the three-argument constructor.

Expected behavior

I expected the non-default Pojo(int productId, String name, int weight) constructor to be used in both cases, and not just with Jackson 2. It is used with Jackson 3 when annotated with @JsonCreator or when the default constructor is removed.

Additional context

No response

Comment From: cowtowncoder

Odd: I would have expected 2.19 also work like 3 because auto-detection for parameters-taking constructor should only work if there are no other public constructors (not even no-args ("default") constructor). (2.18 and 2.19 as Creator detection was rewritten for 2.18.0).

Comment From: mikereiche

Edit: my issue isn't with JsonCreator. My issue is that the deserialization assumes that the serialized value is one of the name() of the enum. With @JsonValue, it is not necessarily the name().

ETurbulenceCategory was serialized/deserialized properly in Jackson 2. In Jackson 3, it is serialized properly, but deserialization fails. (the strings have an embedded double-quote in them - that is unrelated to the deserialization problem).

Annotating the constructor with @JsonCreator does not change the outcome.

Caused by: tools.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `org.springframework.data.couchbase.domain.ETurbulenceCategory` from String ""10%": not one of the values accepted for Enum class: [T30, T20, T10]
 at [No location information]
        at tools.jackson.databind.exc.InvalidFormatException.from(InvalidFormatException.java:37)
        at tools.jackson.databind.DeserializationContext.weirdStringException(DeserializationContext.java:1977)
        at tools.jackson.databind.DeserializationContext.handleWeirdStringValue(DeserializationContext.java:1320)
        at tools.jackson.databind.deser.jdk.EnumDeserializer._deserializeAltString(EnumDeserializer.java:355)
        at tools.jackson.databind.deser.jdk.EnumDeserializer._fromString(EnumDeserializer.java:215)
        at tools.jackson.databind.deser.jdk.EnumDeserializer.deserialize(EnumDeserializer.java:184)
        at tools.jackson.databind.ObjectMapper._convert(ObjectMapper.java:2391)
        at tools.jackson.databind.ObjectMapper.convertValue(ObjectMapper.java:2323)
        at org.springframework.data.couchbase.core.convert.StringToEnumConverterFactory$StringToEnum.convert(StringToEnumConverterFactory.java:74)
        at org.springframework.data.couchbase.core.convert.StringToEnumConverterFactory$StringToEnum.convert(StringToEnumConverterFactory.java:58)
import com.fasterxml.jackson.annotation.JsonValue;

/**
 * An enum that has an String getCode() method.
 */
public enum ETurbulenceCategory {
    T10("\"10%"), T20("\"20%"), T30("\"30%");

    private final String code;

    ETurbulenceCategory(String code) {
        this.code = code;
    }

    @JsonValue
    public String getCode() {
        return code;
    }
}

Comment From: cowtowncoder

@mikereiche Please file a separate issue: this may or may not be related, but it does not look like same issue.

And if so, please include full reproduction (with input JSON).

Comment From: JooHyukKim

This may break many application... should this be fixed before 3 release? I will add test in 3.x first.

Comment From: cowtowncoder

@JooHyukKim I am not sure how common a problem this is, but definitely worth considering.

I think that:

  1. Behavior of 2.19 (with parameter name detection) and 3.0 should be same (so fix to that if need be)
  2. Problem may be due to existence of public no-args Constructor (or just existence in general) -- but logic since 2.18 (of 2.x) should not differ from 3.0 (behavior of 2.17 and before can be considered irrelevant; 2.18 is the new baseline).

Comment From: cowtowncoder

One additional thought: could it be this did NOT actually work via Constructor but due to default of MapperFeature.ALLOW_FINAL_FIELDS_AS_MUTATORS being true in 2.x (to set properties that way) but false in 3.0? Would it be easy to verify?

I am just trying to understand observed discrepancy where none should exist.

Comment From: wilkinsona

It certainly seems to be involved, @cowtowncoder. Modified reproducer:

package com.example;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;

import tools.jackson.databind.json.JsonMapper;

public class Example {

    public static void main(String[] args) throws Exception {
        String json = "{\"productId\":5, \"name\":\"test\", \"weight\":42}";
        JsonMapper jackson3Mapper = tools.jackson.databind.json.JsonMapper.builder()
                .enable(tools.jackson.databind.MapperFeature.ALLOW_FINAL_FIELDS_AS_MUTATORS)
                .build();
        System.out.println(jackson3Mapper.readValue(json, Pojo.class).getProductId());
        ObjectMapper jackson2Mapper = com.fasterxml.jackson.databind.json.JsonMapper.builder()
                .build();
        jackson2Mapper.registerModule(new ParameterNamesModule());
        System.out.println(jackson2Mapper.readValue(json, Pojo.class).getProductId());
    }

    public static class Pojo {

        private final int productId;

        private final String name;

        private final int weight;

        public Pojo() {
            this(0, null, 0);
        }

        public Pojo(int productId, String name, int weight) {
            System.out.println("Pojo(%s, %s, %s)".formatted(productId, name, weight));
            this.productId = productId;
            this.name = name;
            this.weight = weight;
        }

        public int getProductId() {
            return this.productId;
        }

        public String getName() {
            return this.name;
        }

        public int getWeight() {
            return this.weight;
        }

    }

}

Output:

Pojo(0, null, 0)
5
Pojo(0, null, 0)
5

So enabling the feature causes Jackson 3 to behave like Jackson 2 in this regard. However, disabling the feature with Jackson 2 does not result in it behaving like Jackson 3. Instead, it fails.

Here's the reproducer for that:

package com.example;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;

import tools.jackson.databind.json.JsonMapper;

public class Example {

    public static void main(String[] args) throws Exception {
        String json = "{\"productId\":5, \"name\":\"test\", \"weight\":42}";
        JsonMapper jackson3Mapper = tools.jackson.databind.json.JsonMapper.builder()
                .build();
        System.out.println(jackson3Mapper.readValue(json, Pojo.class).getProductId());
        ObjectMapper jackson2Mapper = com.fasterxml.jackson.databind.json.JsonMapper.builder()
                .disable(com.fasterxml.jackson.databind.MapperFeature.ALLOW_FINAL_FIELDS_AS_MUTATORS)
                .build();
        jackson2Mapper.registerModule(new ParameterNamesModule());
        System.out.println(jackson2Mapper.readValue(json, Pojo.class).getProductId());
    }

    public static class Pojo {

        private final int productId;

        private final String name;

        private final int weight;

        public Pojo() {
            this(0, null, 0);
        }

        public Pojo(int productId, String name, int weight) {
            System.out.println("Pojo(%s, %s, %s)".formatted(productId, name, weight));
            this.productId = productId;
            this.name = name;
            this.weight = weight;
        }

        public int getProductId() {
            return this.productId;
        }

        public String getName() {
            return this.name;
        }

        public int getWeight() {
            return this.weight;
        }

    }

}

And the resulting output:

Pojo(0, null, 0)
0
Pojo(0, null, 0)
Exception in thread "main" com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "productId" (class com.example.Example$Pojo), not marked as ignorable (0 known properties: ])
 at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 1, column: 15] (through reference chain: com.example.Example$Pojo["productId"])
    at com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException.from(UnrecognizedPropertyException.java:61)
    at com.fasterxml.jackson.databind.DeserializationContext.handleUnknownProperty(DeserializationContext.java:1180)
    at com.fasterxml.jackson.databind.deser.std.StdDeserializer.handleUnknownProperty(StdDeserializer.java:2244)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownProperty(BeanDeserializerBase.java:1823)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownVanilla(BeanDeserializerBase.java:1801)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:308)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:169)
    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 com.example.Example.main(Example.java:19)

Comment From: cowtowncoder

@wilkinsona Ok, yes, based on output it indeed looks like:

  1. No-args constructor is being used for both 2.x and 3.0, and
  2. By default, with Jackson 2.x, final Fields are used to set values

and so Creator detection does not include 3-arg constructor.

But the question then is rather whether we ought to allow auto-detection in 3.x (for 2.x I am bit more worried about regression) to compensate. Specifically: should rules of properties-taking Constructor auto-detection allow case of 0-args construtor (but no others) -- something not allowed at this point.

And if we do this, would it be necessary to add a MapperFeature to allow preventing such detection?

My only concern here is that some use case somewhere was relying on auto-detection not working. But... for 3.0, major change, maybe there is little risk.

Comment From: wilkinsona

I think my main concern here is the deserialisation change going unnoticed when upgrading from Jackson 2 to Jackson 3. Given that Jackson 3 has the feature disabled by default (FWIW, not mutating final fields seems to me like a good change to the defaults), I wonder if it could be made to behave as Jackson 2 does when the feature is disabled? That would make Jackson 3 fail fast out of the box in this situation rather than giving you a POJO with its default values. That feels to me like it would be safer and would increase the chances of early discovery of the problem.

Comment From: cowtowncoder

@wilkinsona Ahhhhhhh. You know what? The last piece of the puzzle is #493 -- DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES change to false in 3.0. This is why failure is hidden. And it's one change I personally do not like, but that is also overwhelmingly favored by user base. :-)

Comment From: cowtowncoder

I'll see if I can change logic in 3.0 to achieve what is desired -- use of 2-parameter un-annotated Constructor despite existence of 0-parameter Constructor. This is change in behavior and cannot be done for 2.x: but is not (sort of) needed due to settings that force override of final Fields (as per my earlier comment).

Comment From: cowtowncoder

I have #5308 that does allow auto-detection even in presence of no-params constructor, but have concerns about compatibility aspects. Will need to add MapperFeature to enable/disable; then decide what is the default.

Comment From: JooHyukKim

So we need

First naming,we may start drafting by literal.

'DETECT_PROPERTIES_BASED_CONSTRUCTOR_WITH_ZERO_PARAM_CONSTRUCTOR'

Second default value,

  • The default value may introduce to easier usage
  • If needed, can add to Jackson2Defaults() whichever side provides compatibility

Comment From: cowtowncoder

Yeah, naming is hard. Thank you for offering good suggestion for a starting point @JooHyukKim !

I'll try enabling by default, seeing how cascading build works (I had to fix ~20 tests in databind itself, pretty sure many other components require changes too).

Comment From: cowtowncoder

One other idea: instead of MapperFeature, I think this should be configured via ConstructorDetector -- more specific, can name, document better.

Note: really want this issue solved for 3.0.0-rc10.

Comment From: cowtowncoder

Replaced by #5318.