-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Description
Describe the bug
In our projects we rely very heavily on Jackson. In one of our recent dependency upgrades, Jackson was bumped from version 2.13.3 to 2.14.0, and we started seeing some odd behavior (mostly in serialization, but it's unclear if this affect deserialization as well). I believe the problems are related to #3357.
The problems manifest when there is a mix of a public field and a getter, and most of the times, it also involves a base class (possibly abstract), that defines the getters as well (as abstract). It happens when there is a mix of @JsonIgnore and @JsonProperty/@JsonView on either of the field/concrete getter/abstract getter (e.g. @JsonProperty on the abstract getter, and then @JsonIgnore on the overridden getter). I had to write a small test to demonstrate the issues... this test demonstrate only some of the issues we experienced, but there could be other issues, since, if I understand correctly, you'v changed the logic to decide when to serialize in cases where there is a mix of @JsonIgnore and @JsonProperty. I must say that, in some cases, when there is inheritance involved, it makes sense to have this kind of mix if the different annotations are added in different levels in the class hierarchy.
See test how to reproduce below.
Version information
2.14.0
To Reproduce
I ran the following test both with version 2.13.3 and 2.14.0:
public class JacksonJsonIgnoreTest {
@Test
public void testChildClassSerialization() throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper().configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
Child child = new Child();
System.out.println(mapper.writeValueAsString(child));
}
abstract static class Base {
@JsonProperty
abstract String getField1();
@JsonProperty
abstract String getField2();
@JsonProperty
abstract String getField3();
}
static class Child extends Base {
// ---------------------------------------------------
// Getter exists in base class, no annotation on field
// ---------------------------------------------------
public String field1 = "field1";
@Override
@JsonIgnore
public String getField1() {
return field1 + "Getter";
}
// -----------------------------------------------
// Getter exists in base class, field is annotated
// -----------------------------------------------
@JsonProperty
public String field2 = "field2";
@Override
@JsonIgnore
public String getField2() {
return field2 + "Getter";
}
// ----------------------------------------------------------------------
// Getter exists in base class, field is ignored, no annotation on getter
// ----------------------------------------------------------------------
@JsonIgnore
public String field3 = "field3";
@Override
public String getField3() {
return field3 + "Getter";
}
// ----------------------------------
// Getter doesn't exist in base class
// ----------------------------------
public String field4 = "field4";
@JsonIgnore
public String getField4() {
return field4 + "Getter";
}
}
}Results:
2.13.3:{"field1":"field1","field2":"field2","field3":"field3Getter"}2.14.0:{"field2":"field2","field3":"field3Getter"}
The interesting things to note here:
- Comparing
field1andfield4- they have the exact same annotations on the field and getters, but the only difference is thatfield1is also defined in the base class (with@JsonProperty) andfield4is not. In version2.13.3,field1was serialized, whereas in2.14.0it is not, whereas in both versionsfield4is not serialized. It seems that the annotation overgetField1in the base class somehow affected serialization of the child class, even though we overrode it (so I would expect the annotation in the child class to win). In2.14.0this works "correctly", but while this behavior is more correct, it's still a big change in semantics (which might be considered a breaking change actually). Notice that there is no annotation onfield1itself (there no explicit@JsonPropertythere), so basically we relied on the default behavior. field2is exactly likefield1, except that it has an explicit@JsonPropertyannotation on the field. In this case, both versions decided to serialize it. So I'm raising a question, what is the difference between explicitly setting the@JsonPropertyannotation (field2) to not setting it at all, and using the default semantics (field1)? Our code relies on the default behavior in lots of places (unfortunately...)field3is the opposite case offield1- the field has an explicit@JsonIgnoreannotation, whereas the getter does not have any annotation (we rely on the default behavior here, again). Apparently both versions serialize it, and treat it as if it has a@JsonPropertyannotation (maybe because of the base class? either way it's very confusing, especially when comparing it to the reverse direction, which isfield1, and to the fact that if I override a method I expect not to inherit its annotations).
These are just a few samples, but of course I didn't test cases where the base class methods don't have annotations (in my tests all of them have @JsonProperty, what if they didn't have this set explicitly, and just rely on the default behavior?). And I haven't tested how this affects deserialization (like, what would happen if there is a contradiction between the annotations on the field and the annotations on the setter? or contradiction between an abstract setter and the concrete setter?)
Expected behavior
TBH it's unclear what should be the correct behavior, as there are many kinds of scenarios. Notice that, as I wrote above, it might make sense that the behavior for field1 will be the one implemented in 2.14.x, but I can't say the same about field3 (even though it's identical in both versions, I would expect some consistency in behavior between the 2 symmetrical cases of field1 and field3). In any case, since the behavior used to be like in version 2.13.x for a very long time, and now it has changed, I think we should consider this a breaking change, and considering the potential risks it impose, maybe worth reverting it to the original behavior. Accepting the changes in 2.14.x would require us to do (probably) lots of changes in our huge code base... and of course understanding the new behavior, and how it's different from the behavior in previous versions.