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:
- Behavior of 2.19 (with parameter name detection) and 3.0 should be same (so fix to that if need be)
- Problem may be due to existence of
publicno-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:
- No-args constructor is being used for both 2.x and 3.0, and
- By default, with Jackson 2.x,
finalFields 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.