Search before asking
- [x] I searched in the issues and found nothing similar.
Describe the bug
The attached class works correctly on Jackson 2 - but errors on Jackson 3.
Version Information
jackson-databind-3.0.0-rc7
Reproduction
Class:
package j3bug;
import java.util.HashMap;
import java.util.Map;
public class DateTimeParserState {
public int sampleCount;
public int nullCount;
public int blankCount;
public int invalidCount;
public final Map<String, Integer> results;
public DateTimeParserState() {
results = new HashMap<>();
}
}
Driver:
package j3bug;
import java.util.Locale;
import tools.jackson.core.StreamReadFeature;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
public abstract class J3bug {
public static void main(String[] args) {
ObjectMapper mapper = JsonMapper.builder().enable(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION).build();
DateTimeParserState state = new DateTimeParserState();
state.results.put("hello", 1);
String serialized = mapper.writeValueAsString(state);
DateTimeParserState post = mapper.readValue(serialized, DateTimeParserState.class);
if (post.results.size() != 1) {
System.err.println("Houston, we have a problem!");
System.exit(1);
}
}
}
Expected behavior
Expected Map to be restored to its former glory (i.e. one entry) when deserialized.
Additional context
No response
Comment From: pjfanning
Can you provide more details? * what is the input JSON? Include it instead of forcing someone to generate it themselves. Have you checked if the JSON that is serialized id what you expect? I've seen people report issues about deserialization where the real reason is that serialization changed and the new output of serialization causes issues when deserializing. * what is the exception when deserializing?
Comment From: tsegall
Apologies for not being clearer. The above program, if executed with Jackson 2 works. If executed with Jackson 3 it does not.
This is not an attempt to take a serialized Jackson 2 string and deserialize it in Jackson 3.
As per the program above - when you run this with Jackson 3 and add one element to the array:
The serialization of the objects is {"blankCount":0,"invalidCount":0,"nullCount":0,"results":{"hello":1},"sampleCount":0}
Post readValue() the object is {"blankCount":0,"invalidCount":0,"nullCount":0,"results":{},"sampleCount":0}
That is it is missing the results Map.
There is no exception when the program is run - just the wrong result.
Comment From: pjfanning
Thanks @tsegall for the extra information. I don't have time in the next few hours to look at this but may get back to it later and someone else may be able to help too. One interesting experiment would be be to try making the 'results' field non-final. There may also be options around adding Jackson annotations. I'm not saying that we want to keep the new behaviour that you are seeing but it might be interesting to find out what works and what doesn't.
Comment From: tsegall
@pjfanning As you clearly suspected :-) it is the final that is causing the issue. Without the final the program works as expected.
Comment From: pjfanning
@tsegall would you consider adding a constructor to your class that sets all the fields? Having such a public constructor means that the deserializer can use a public API to create the class instance instead of relying on reflection APIs that ignore private and final markers on fields. Java is making it harder to use the Reflection backdoors for security reasons.
Comment From: cowtowncoder
If it's about final
Field, I think it's due to
https://github.com/FasterXML/jackson-databind/blob/3.x/src/main/java/tools/jackson/databind/MapperFeature.java#L83
change via https://github.com/FasterXML/jackson-databind/issues/4552
where default for MapperFeature.ALLOW_FINAL_FIELDS_AS_MUTATORS
was changed to false
(as mentioned on https://github.com/FasterXML/jackson-future-ideas/wiki/JSTEP-2.
So it is possible to just enable this feature. But one caveat is that this feature may not work in newer JVMs.
Comment From: cowtowncoder
One thing that sounds and seems wrong is the lack of exception: but this is due to another defaults change:
https://github.com/FasterXML/jackson-databind/issues/493
which is against my personal preferences, but strongly supported by user base it seems.
So basically now unknown (since final
fields are not considered as Mutators) properties are just ignored by default.
This default can be changed for mapper too of course.
So I think this is what is going on here.
Comment From: tsegall
@cowtowncoder as @pjfanning suggested I added a public constructor that sets all the fields - but this did not help (it was not invoked).
So it does not sound like I should enable MapperFeature.ALLOW_FINAL_FIELDS_AS_MUTATORS as this is going to be a bad idea moving forward.
So it seems I can either remove the final or make results private and add getters and setters.
Thoughts?
Comment From: cowtowncoder
@tsegall Please share your code for constructor add. It will work, but depending on details you may need to annotate constructor with @JsonCreator
.
Also, for testing at least, consider enabling DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
as that often uncovers naming mismatches.
As to enabling MapperFeature.ALLOW_FINAL_FIELDS_AS_MUTATORS
, yeah, it is ideally not relied on as it depends on specific quirk of JVM allowing mutation of "final" fields for brief period of time after insantiation (to support JDK serialization I think).
Comment From: tsegall
@cowtowncoder I added the following
public DateTimeParserState(final int sampleCount, final int nullCount, final int blankCount, final int invalidCount, final Map<String, Integer> results) {
System.err.println("Contructor invoked");
this.sampleCount = sampleCount;
this.nullCount = nullCount;
this.blankCount = blankCount;
this.invalidCount = invalidCount;
this.results = results;
}
Interestingly if I do as you suggest and add this DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES then I do get an error as opposed to a silent failure.
It seems to be ignoring the results property because it is final. Note: If I remove the final it will call the new constructor above and all works.
Exception in thread "main" tools.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized property "results" (class j3bug.DateTimeParserState), not marked as ignorable (4 known properties: "blankCount", "invalidCount", "nullCount", "sampleCount"])
at [Source: (String)"{"blankCount":0,"invalidCount":0,"nullCount":0,"results":{"hello":1},"sampleCount":0}"; line: 1, column: 59] (through reference chain: j3bug.DateTimeParserState["results"])
at tools.jackson.databind.exc.UnrecognizedPropertyException.from(UnrecognizedPropertyException.java:51)
at tools.jackson.databind.DeserializationContext.handleUnknownProperty(DeserializationContext.java:1269)
at tools.jackson.databind.deser.std.StdDeserializer.handleUnknownProperty(StdDeserializer.java:2001)
at tools.jackson.databind.deser.bean.BeanDeserializerBase.handleUnknownProperty(BeanDeserializerBase.java:1724)
at tools.jackson.databind.deser.bean.BeanDeserializerBase.handleUnknownVanilla(BeanDeserializerBase.java:1702)
at tools.jackson.databind.deser.bean.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:545)
at tools.jackson.databind.deser.bean.BeanDeserializer.deserialize(BeanDeserializer.java:200)
at tools.jackson.databind.deser.DeserializationContextExt.readRootValue(DeserializationContextExt.java:265)
at tools.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:2688)
at tools.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:1600)
at j3bug.J3bug.main(J3bug.java:23)
Comment From: cowtowncoder
@tsegall Hmmm. final
should not matter for constructor. Let me see if I can reproduce the problem.
Comment From: cowtowncoder
@tsegall Cannot replicate, see #5255. Will try to change to actually use Map
, should not really matter.
So need help reproducing the issue.
Comment From: tsegall
@cowtowncoder The following reproduces the issue for me.
Class:
package j3bug;
import java.util.HashMap;
import java.util.Map;
public class DateTimeParserState {
public int sampleCount;
public int nullCount;
public int blankCount;
public int invalidCount;
public final Map<String, Integer> results;
public DateTimeParserState(final int sampleCount, final int nullCount, final int blankCount, final int invalidCount, final Map<String, Integer> results) {
System.err.println("Contructor invoked");
this.sampleCount = sampleCount;
this.nullCount = nullCount;
this.blankCount = blankCount;
this.invalidCount = invalidCount;
this.results = results;
}
public DateTimeParserState() {
System.err.println("Contructor small invoked");
results = new HashMap<>();
}
}
Driver:
package j3bug;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import tools.jackson.core.StreamReadFeature;
import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
public abstract class J3bug {
public static void main(String[] args) {
ObjectMapper mapper = JsonMapper.builder().enable(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION)
.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.build();
final Map<String, Integer> notMuch = new HashMap<>();
notMuch.put("hello", 1);
DateTimeParserState state = new DateTimeParserState(0, 0, 0, 0, notMuch);
String serialized = mapper.writeValueAsString(state);
System.err.println("pre serialized = " + serialized);
DateTimeParserState post = mapper.readValue(serialized, DateTimeParserState.class);
System.err.println("post serialized = " + mapper.writeValueAsString(post));
if (post.results.size() != 1) {
System.err.println("Houston, we have a problem!");
System.exit(1);
}
}
}