Describe the bug
Currently Jackson annotations (in my case @JsonProperty) is only copied properly on Builder methods if the Collection is not annotated with @Singular. This makes deserialization with Jackson completely reliant on having the field named the same way it will be in the JSON file with no additional flexibility.
To Reproduce
Here is an example of the source code with and without the Singular annotation vs the decompiled classes.
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
import lombok.Builder;
import lombok.Singular;
import java.util.Collection;
@Builder
@JsonDeserialize(builder = SomePojo.SomePojoBuilder.class)
public class SomePojo {
@JsonProperty("foos")
private Collection<String> notFoos;
@Singular
@JsonProperty("bars")
private Collection<String> notBars;
@JsonPOJOBuilder(withPrefix = "")
public static class SomePojoBuilder {
}
}
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@JsonDeserialize(
builder = SomePojo.SomePojoBuilder.class
)
public class SomePojo {
@JsonProperty("foos")
private Collection<String> notFoos;
@JsonProperty("bars")
private Collection<String> notBars;
SomePojo(@JsonProperty("foos") Collection<String> notFoos, @JsonProperty("bars") Collection<String> notBars) {
this.notFoos = notFoos;
this.notBars = notBars;
}
public static SomePojo.SomePojoBuilder builder() {
return new SomePojo.SomePojoBuilder();
}
@JsonPOJOBuilder(
withPrefix = ""
)
public static class SomePojoBuilder {
private Collection<String> notFoos;
private ArrayList<String> notBars;
SomePojoBuilder() {
}
@JsonProperty("foos")
public SomePojo.SomePojoBuilder notFoos(@JsonProperty("foos") Collection<String> notFoos) {
this.notFoos = notFoos;
return this;
}
public SomePojo.SomePojoBuilder notBar(String notBar) {
if (this.notBars == null) {
this.notBars = new ArrayList();
}
this.notBars.add(notBar);
return this;
}
public SomePojo.SomePojoBuilder notBars(Collection<? extends String> notBars) {
if (this.notBars == null) {
this.notBars = new ArrayList();
}
this.notBars.addAll(notBars);
return this;
}
public SomePojo.SomePojoBuilder clearNotBars() {
if (this.notBars != null) {
this.notBars.clear();
}
return this;
}
public SomePojo build() {
List notBars;
switch(this.notBars == null ? 0 : this.notBars.size()) {
case 0:
notBars = Collections.emptyList();
break;
case 1:
notBars = Collections.singletonList((String)this.notBars.get(0));
break;
default:
notBars = Collections.unmodifiableList(new ArrayList(this.notBars));
}
return new SomePojo(this.notFoos, notBars);
}
public String toString() {
return "SomePojo.SomePojoBuilder(notFoos=" + this.notFoos + ", notBars=" + this.notBars + ")";
}
}
}
Here is a test case to further illustrate the difference in behavior between Singular vs . non-Singular
import static org.assertj.core.api.Assertions.assertThat;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import java.util.Collections;
public class SomePojoTest {
@Test
public void testDeserialization() throws JsonProcessingException {
final ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
final SomePojo expected = SomePojo.builder().notFoos(Collections.singleton("Hello")).notBar(("World!")).build();
final SomePojo actual = mapper.readValue("{\"foos\":[\"Hello\"],\"bars\":[\"World!\"]}", SomePojo.class);
assertThat(actual).usingRecursiveComparison().isEqualTo(expected);
}
}
The assertion fails as follows:
field/property 'notBars' differ:
- actual value : []
- expected value : ["World!"]
actual and expected values are collections of different size, actual size=0 when expected size=1
My lombok.config
config.stopBubbling = true
lombok.copyableAnnotations += com.fasterxml.jackson.annotation.JsonProperty
Expected behavior
Inside the builder the notBars(Collection<? extends String> notBars) should receive a copy of the @JsonProperty annotations, so that the aforementioned test case can pass without renaming the field notBars to bars (which means @JsonProperty is redundant)
Version info (please complete the following information):
Fixed with PR #2429. Will work out of the box, so no config settings required.
Most helpful comment
Fixed with PR #2429. Will work out of the box, so no config settings required.