Skip to content

Serialization behavior changed in 2.14 when mixing @JsonIgnore and @JsonProperty on different levels of class hierarchy and/or accessors #3722

Closed
@RB14

Description

@RB14

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 and field4 - they have the exact same annotations on the field and getters, but the only difference is that field1 is also defined in the base class (with @JsonProperty) and field4 is not. In version 2.13.3, field1 was serialized, whereas in 2.14.0 it is not, whereas in both versions field4 is not serialized. It seems that the annotation over getField1 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). In 2.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 on field1 itself (there no explicit @JsonProperty there), so basically we relied on the default behavior.
  • field2 is exactly like field1, 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 of field1 - 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 is field1, 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions