I am trying to merge JSON objects with the new @JsonMerge
annotation. I found a sample online that works when I run it in my IDE. Here's a snippet to run:
@Test
void mergeTest() throws IOException {
final Employee employee = new Employee("Serializon", new Address("Street 1", "City 1", "ZipCode1"));
final Employee newEmployee = new Employee("Serializon", new Address("Street 2", "City 2", "ZipCode2"));
assertThat(employee.getAddress().getCity()).isEqualTo("City 1");
final ObjectMapper objectMapper = new ObjectMapper();
final Employee mergedEmployee = objectMapper.readerForUpdating(employee).readValue(JSONUtil.toJSON(newEmployee));
System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(mergedEmployee));
assertThat(newEmployee.getAddress().getCity()).isEqualTo("City 2");
assertThat(mergedEmployee.getAddress().getCity()).isEqualTo("City 2");
}
public class Employee {
private String name;
@JsonMerge
private Address address;
public Employee(final String name, final Address address) {
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public Address getAddress() {
return address;
}
}
public class Address {
private String street;
private String city;
private String zipCode;
public Address(final String street, final String city, final String zipCode) {
this.street = street;
this.city = city;
this.zipCode = zipCode;
}
public String getStreet() {
return street;
}
public String getCity() {
return city;
}
public String getZipCode() {
return zipCode;
}
}
When I try to reproduce this with my own class, it fails with the following error:
Deserialization of [simple type, class package.State] by passing existing instance (of package.State) not supported
My class in question is a POJO with some lists and primitive properties, all with getters. It is constructed using a builder and is immutable. It looks like this:
@JsonDeserialize(builder = State.Builder.class)
public class State {
private final String id;
@JsonMerge
private final List<Module> modules;
protected State(final Builder builder) {
this.id = builder.id;
this.modules = builder.modules;
}
public static Builder builder() {
return new Builder();
}
public String getId() {
return id;
}
public List<Module> getModules() {
return modules;
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static final class Builder {
private String id;
private List<Module> modules;
private Builder() {
}
public Builder withId(final String id) {
this.id = id;
return this;
}
public Builder withModules(final List<Module> modules) {
this.modules = modules;
return this;
}
public State build() {
return new State(this);
}
}
}
The merge annotation states the following:
Merging is only option if there is a way to introspect current state: if there is accessor (getter, field) to use. Merging can not be enabled if no accessor exists or if assignment occurs using a Creator setter (constructor or factory method), since there is no instance with state to introspect.
So I thought perhaps the builder might the problem, but retrofitting the Employee/Address sample with a builder still works:
@Test
void mergeTest() throws IOException {
final Employee employee = Employee.newBuilder()
.withName("Serializon")
.withAddress(Address.newBuilder()
.withStreet("Steet 1")
.withCity("City 1")
.withZipCode("ZipCode1")
.build())
.build();
assertThat(employee.getAddress().getCity()).isEqualTo("City 1");
final Employee newEmployee = Employee.newBuilder()
.withName("Serializon")
.withAddress(Address.newBuilder()
.withStreet("Steet 2")
.withCity("City 2")
.withZipCode("ZipCode2")
.build())
.build();
final ObjectMapper objectMapper = new ObjectMapper();
final Employee mergedEmployee = objectMapper.readerForUpdating(employee).readValue(JSONUtil.toJSON(newEmployee));
System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(mergedEmployee));
assertThat(newEmployee.getAddress().getCity()).isEqualTo("City 2");
assertThat(mergedEmployee.getAddress().getCity()).isEqualTo("City 2");
}
@JsonDeserialize(builder = Employee.Builder.class)
public class Employee {
private String name;
@JsonMerge
private Address address;
private Employee(final Builder builder) {
name = builder.name;
address = builder.address;
}
public static Builder newBuilder() {
return new Builder();
}
public String getName() {
return name;
}
public Address getAddress() {
return address;
}
public static final class Builder {
private String name;
private Address address;
private Builder() {
}
public Builder withName(final String name) {
this.name = name;
return this;
}
public Builder withAddress(final Address address) {
this.address = address;
return this;
}
public Employee build() {
return new Employee(this);
}
}
}
@JsonDeserialize(builder = Address.Builder.class)
public class Address {
private String street;
private String city;
private String zipCode;
private Address(final Builder builder) {
street = builder.street;
city = builder.city;
zipCode = builder.zipCode;
}
public static Builder newBuilder() {
return new Builder();
}
public String getStreet() {
return street;
}
public String getCity() {
return city;
}
public String getZipCode() {
return zipCode;
}
public static final class Builder {
private String street;
private String city;
private String zipCode;
private Builder() {
}
public Builder withStreet(final String street) {
this.street = street;
return this;
}
public Builder withCity(final String city) {
this.city = city;
return this;
}
public Builder withZipCode(final String zipCode) {
this.zipCode = zipCode;
return this;
}
public Address build() {
return new Address(this);
}
}
}
Finally I tried to have a list of addresses instead, and accepting the list in the builder as withAddresses
instead. So, for brevity:
@JsonDeserialize(builder = Employee.Builder.class)
public class Employee {
@JsonMerge
private List<Address> addresses;
public static final class Builder {
public Builder withAddresses(final List<Address> addresses) {
this.addresses = addresses;
return this;
}
}
}
And when I run the testcase again, this fails with the same error as my own code:
Deserialization of [simple type, class se.itab.locker.core.util.Employee] by passing existing instance (of se.itab.locker.core.util.Employee) not supported
What is actually going on here, and can I resolve it somehow or is this an unsupported use case or bug?
Update So I found that this works:
//@JsonDeserialize(builder = Employee.Builder.class)
public class Employee {
@JsonCreator
public Employee(final Employee employee) {
name = employee.name;
addresses = employee.addresses;
stringAddresses = employee.stringAddresses;
}
But then serializing causes an infinite loop instead.