Why do my field-based Jackson mixins break when using Lombok @Data?

Viewed 565

I have a project which uses both Jackson and Lombok. It is using the current latest versions of both (Jackson 2.12.2 & Lombok 1.18.18). I have the following class containing a Jackson mapped object, with an associated Jackson mixin, allowing it to be deserialized either ignoring the given field or not, depending on whether the mixin is passed to the ObjectMapper:

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.json.JsonMapper;

public class IssueDemonstration {
    public static void main(String[] args) throws JsonProcessingException {
        JsonMapper jsonMapper = JsonMapper.builder().build();
        JsonMapper mixinMapper = JsonMapper.builder()
                .addMixIn(Example.class, ExampleMixin.class).build();

        String json = "{\"value\": \"Some Value\"}";
        System.out.println(
                "Default: " + jsonMapper.readValue(json, Example.class).getValue());
        System.out.println(
                "Mixin: " + mixinMapper.readValue(json, Example.class).getValue());
    }

    private static class Example {
        @JsonProperty(access = JsonProperty.Access.READ_ONLY)
        private String value;

        public String getValue() {
            return value;
        }

        public void setValue(String value) {
            this.value = value;
        }
    }

    private abstract static class ExampleMixin {
        @JsonProperty(access = JsonProperty.Access.READ_WRITE)
        private String value;
    }
}

The above code gives the expected results:

Default: null
Mixin: Some Value

However, if I change Example to use Lombok's @Data to automagically generate the getter & setter, the mixin no longer works:

@Data
private static class Example {
    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
    private String value;
}

Output:

Default: null
Mixin: null

Why is this not working when I switch to using @Data? I see nothing in the Jackson @Data or @Getter and @Setter documentation that indicate that this should behave in this manner.

2 Answers

The issue is that Lombok is copying the Jackson @JsonProperty annotation to the generated setter. In Jackson, annotations on accessor methods take precedence over the same annotations on fields. Therefore, the annotation on the getValue() method in the Example class is overriding the annotation on the value field in ExampleMixin.

This can be fixed in a number of ways. The mixin can be changed to have a Jackson-annotated setter (thus triggering an override of the Lombok setter), either manually or by annotating the mixin field using @Setter:

private interface ExampleMixin {
    @JsonProperty(access = JsonProperty.Access.READ_WRITE)
    void setValue(String value);
}
private abstract static class ExampleMixin {
    @Setter
    @JsonProperty(access = JsonProperty.Access.READ_WRITE)
    private String value;
}

Alternatively, the deserialized class can be changed to have an unannotated manual setter created for it in addition to the annotated field, thus preventing Lombok from creating an annotated one:

@Data
private static class Example {
    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
    private String value;

    public void setValue(String value) {
        this.value = value;
    }
}

One tricky thing about this whole situation is the fact that the copying of Jackson annotations to the setter does not appear documented in Lombok. The documentation mentions nullability annotations as being copied over, but makes no mention of the copied Jackson annotations:

lombok.copyableAnnotations = [A list of fully qualified types] (default: empty list) Lombok will copy any of these annotations from the field to the setter parameter, and to the getter method. Note that lombok ships with a bunch of annotations 'out of the box' which are known to be copyable: All popular nullable/nonnull annotations.

Various well-known annotations about nullability, such as org.eclipse.jdt.annotation.NonNull, are automatically copied over to the right place (method for getters, parameter for setters). You can specify additional annotations that should always be copied via lombok configuration key lombok.copyableAnnotations.

A look at the Lombok source code, however, reveals the copied Jackson annotations:

COPY_TO_SETTER_ANNOTATIONS = Collections.unmodifiableList(Arrays.asList(new String[] {
    "com.fasterxml.jackson.annotation.JacksonInject",
    "com.fasterxml.jackson.annotation.JsonAlias",
    "com.fasterxml.jackson.annotation.JsonFormat",
    "com.fasterxml.jackson.annotation.JsonIgnore",
    "com.fasterxml.jackson.annotation.JsonIgnoreProperties",
    "com.fasterxml.jackson.annotation.JsonProperty",
    "com.fasterxml.jackson.annotation.JsonSetter",
    "com.fasterxml.jackson.annotation.JsonSubTypes",
    "com.fasterxml.jackson.annotation.JsonTypeInfo",
    "com.fasterxml.jackson.annotation.JsonView",
    "com.fasterxml.jackson.databind.annotation.JsonDeserialize",
    "com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty",
}));

The mixin field issue, including the lack of documentation, is captured as issue #2769 in the Lombok issue tracker.

Lombok @Data is equivalent to @Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode.

Default constructor is without parameters, and lombok @Data have parameters. You need to add @NoArgsConstructor.

    @Data
    @NoArgsConstructor
    private static class Example {
        @JsonProperty(access = JsonProperty.Access.READ_ONLY)
        private String value;
    }

    @Data
    @NoArgsConstructor
    private abstract static class ExampleMixin {
        @JsonProperty(access = JsonProperty.Access.READ_WRITE)
        private String value;
    }
Related