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
field1
andfield4
- they have the exact same annotations on the field and getters, but the only difference is thatfield1
is also defined in the base class (with@JsonProperty
) andfield4
is not. In version2.13.3
,field1
was serialized, whereas in2.14.0
it is not, whereas in both versionsfield4
is not serialized. It seems that the annotation overgetField1
in 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.0
this 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 onfield1
itself (there no explicit@JsonProperty
there), so basically we relied on the default behavior. field2
is exactly likefield1
, except that it has an explicit@JsonProperty
annotation 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@JsonProperty
annotation (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...)field3
is the opposite case offield1
- the field has an explicit@JsonIgnore
annotation, 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@JsonProperty
annotation (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.