@JsonMerge with list of objects and builder

1.2k Views Asked by At

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.

0

There are 0 best solutions below