Search code examples
javaobjectmapper

Java convert object into abstract object


I am using SpringBoot with Java 1.8.

I have two objects that I would like to deep copy one to the other.

Basic structure

QuoteRequestDTO -> TravelRequirementDTO -> ItineraryDTO -> ServiceDTO

and

QuoteRequest -> TravelRequirement -> Itinerary -> Service

note: the Entity objects come from an external library I cannot change. I can change the DTO objects.

For example, I want to copy a DTO to an Entity.

DTO

public class QuoteRequestDTO {    
    protected TravelRequirementDTO travel;
    ...

and

public class TravelRequirementDTO {
    protected ItineraryDTO required;
    ...

and

public class ItineraryDTO extends PayableDTO {
    protected List<ServiceDTO> service;
    ...

and

public class ServiceDTO extends PayableDTO {
    ...

Entity

public class QuoteRequest {
    protected TravelRequirement travel;
    ...

and

public class TravelRequirement implements Serializable {
    private static final long serialVersionUID = 1L;
    protected Itinerary required;
    ...

and

public class Itinerary extends Payable implements Serializable {
    private static final long serialVersionUID = 1L;
    protected List<Service> service;
    ...

and

public abstract class Service extends Payable implements Serializable {
    private static final long serialVersionUID = 1L;
    ...

Copy Utility

I have tried the following:

import com.fasterxml.jackson.databind.ObjectMapper;

public class CopyUtils {

    public static <T>T deepCopy(Object sourceObject, T targetObject) {
        ObjectMapper mapper = getJacksonObjectMapper();
        T targetBean = (T) mapper.convertValue(sourceObject, targetObject.getClass());
        return targetBean;
    }

    private static ObjectMapper getJacksonObjectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.findAndRegisterModules();
        objectMapper.configure(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        return objectMapper;
    }
}

Usage:

public void getQuote(QuoteRequestDTO quoteRequestDTO) {
    QuoteRequest quoteRequest = new QuoteRequest();
    quoteRequest = CopyUtils.deepCopy(quoteRequestDTO, quoteRequest);

Error

It gets the following error:

Cannot construct instance of com.mycompany.transit._2008a.Service (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information at [Source: UNKNOWN; byte offset: #UNKNOWN] (through reference chain: com.mycompany.transit._2008a.availability.QuoteRequest["travel"]->com.mycompany.transit._2008a.TravelRequirement["required"]->com.mycompany.transit._2008a.Itinerary["service"]->java.util.ArrayList[0])

When I change the ServiceDTO to be an abstract class:

public abstract class ServiceDTO extends PayableDTO implements Serializable {
    private static final long serialVersionUID = 1L;

It get the following error:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of com.mycompany.restosgi.dto.transit.ServiceDTO (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 13, column: 13] (through reference chain: com.mycompany.restosgi.dto.transit.availability.QuoteRequestDTO["travel"]->com.mycompany.restosgi.dto.transit.TravelRequirementDTO["required"]->com.mycompany.restosgi.dto.transit.ItineraryDTO["service"]->java.util.ArrayList[0])

Question

Is there a way I can write a generic utility method to deep copy objects to another object that has abstract objects?

Possible solution

Is there a way to add a converter that creates the relevant concrete class (implementation)?

The Service needs to be an implementation, for example a TransitService.

e.g.

public class TransitService extends Service implements Serializable {
    private static final long serialVersionUID = 1L;

Possible Solution

As per the advise from Delta George below, I am trying the following:

public class ItineraryDTO extends PayableDTO {
    protected List<ServiceDTO> service;

    @JsonAnySetter
    public void serService(String key, ArrayNode array) {
        service = new ArrayList<>();
        array.forEach(json -> service.add(toService(json)));
    }

    private ServiceDTO toService(JsonNode json) {
        if (json.has("some unique property of flight")) {
            return new ObjectMapper().convertValue(json, FlightDTO.class);
        } else if (json.has("some unique property of transit")) {
            return new ObjectMapper().convertValue(json, TransitServiceDTO.class);
        } else return null;
    }

    ...

However, I cannot get it to invoke the serService method. I think I need to add some Spring config to do so?

Also, if I make the ServiceDTO abstract, it get the following error.

public abstract class ServiceDTO extends PayableDTO {

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of com.clubtravel.restosgi.dto.transit.ServiceDTO (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 13, column: 13] (through reference chain: com.clubtravel.restosgi.dto.transit.availability.QuoteRequestDTO["travel"]->com.clubtravel.restosgi.dto.transit.TravelRequirementDTO["required"]->com.clubtravel.restosgi.dto.transit.ItineraryDTO["service"]->java.util.ArrayList[0]) at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67) ~[jackson-databind-2.13.2.1.jar:2.13.2.1]


Solution

  • A concept implementation using Jackson's @JsonAnySetter to intercept incoming objects as an array of JSON trees and convert each to a concrete POJO based on the structure of each object:

    @Data
    @NoArgsConstructor
    static abstract class Person {
    }
    
    @Data
    @NoArgsConstructor
    static class PersonA extends Person {
        String a;
    }
    
    @Data
    @NoArgsConstructor
    static class PersonB extends Person {
        String b;
    }
    
    @NoArgsConstructor
    @ToString
    static class People {
        List<Person> team;
    
        @JsonAnySetter
        public void setTeam(String key, ArrayNode array) {
            team = new ArrayList<>();
            array.forEach(json -> team.add(toPerson(json)));
        }
    
        private Person toPerson(JsonNode json) {
            if (json.has("a")) {
                return new ObjectMapper().convertValue(json, PersonA.class);
            } else if (json.has("b")) {
                return new ObjectMapper().convertValue(json, PersonB.class);
            } else return null;
        }
    }
    
    public static void main(String[] args) throws JsonProcessingException {
        String json = "{\"team\": [{\"a\": 123}, {\"b\": 45}]}";
    
        People people = new ObjectMapper().readValue(json, People.class);
        System.out.println(people);
    
        // Prints: People(team=[PersonA(a=123), PersonB(b=45)])
    }
    
    // back to the OP's data model
    
    public static class QuoteRequest {
        protected TravelRequirement travel;
    
        public TravelRequirement getTravel() {
            return travel;
        }
    
        public void setTravel(TravelRequirement travel) {
            this.travel = travel;
        }
    }
    
    public static class TravelRequirement implements Serializable {
        private static final long serialVersionUID = 1L;
        protected Itinerary required;
    
        public Itinerary getRequired() {
            return required;
        }
    
        public void setRequired(Itinerary required) {
            this.required = required;
        }
    }
    
    public static class Itinerary extends Payable implements Serializable {
        private static final long serialVersionUID = 1L;
        protected List<Service> service;
    
        public void service(List<Service> service) {
            this.service = service;
        }
    
        @JsonAnyGetter
        public Map<String, Object> getService() {
            return Map.of("service", service);
        }
    
        @JsonAnySetter
        public void setService(String key, ArrayNode array) {
            service = new ArrayList<>();
            array.forEach(json -> service.add(toService(json)));
        }
    
        private Service toService(JsonNode json) {
            return getJacksonObjectMapper().convertValue(json, TransitService.class);
        }
    }
    
    public static abstract class Service extends Payable implements Serializable {
        private static final long serialVersionUID = 1L;
    }
    
    public static class TransitService extends Service implements Serializable {
        private static final long serialVersionUID = 1L;
    }
    
    public static class Payable implements Serializable {
        private static final long serialVersionUID = 1L;
    }
    
    private static ObjectMapper getJacksonObjectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.findAndRegisterModules();
        objectMapper.configure(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        return objectMapper;
    }
    
    public static void main(String[] args) throws JsonProcessingException {
        QuoteRequest qr = new QuoteRequest();
        TravelRequirement tr = new TravelRequirement();
        Itinerary i = new Itinerary();
        i.service(List.of(new TransitService()));
        tr.setRequired(i);
        qr.setTravel(tr);
    
        ObjectMapper mapper = getJacksonObjectMapper();
        QuoteRequest qr2 = mapper.convertValue(qr, QuoteRequest.class);
        System.out.println(qr2);
    }