Java generics: Map nested json response to Java objects

Tags: ,



Scenario: I’m working with an unusual external API in which every attribute is a map with multiple values. In order to convert this response into simple Java objects, I had to do some dirty unboxing. Below is one of the typical java class. As you can see how I’m unboxing data from response and mapping them to my java class:

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.*;

import java.time.LocalDate;
import java.util.Map;

import static com.my.util.BaseUtil.unbox;

@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PartyDetailsDto {

    private String partyId;
    private String partyType;
    private String title;
    private String firstName;
    private String lastName;
    private String middleName;
    private LocalDate dateOfBirth;

    @JsonProperty(value = "partyId")
    public void unboxPartyId(Map<String, String> data) {
        this.partyId = unbox(data, "evidenceId");
    }

    @JsonProperty(value = "partyType")
    public void unboxPartyType(Map<String, String> partyType) {
        this.partyType = unbox(partyType, "value");
    }

    @JsonProperty(value = "individual")
    public void unboxIndividualDetails(Map<String, Object> individual) {
        Map<String, String> title = (Map<String, String>) individual.get("title");
        Map<String, String> firstName = (Map<String, String>) individual.get("firstName");
        Map<String, String> lastName = (Map<String, String>) individual.get("lastName");
        Map<String, String> middleName = (Map<String, String>) individual.get("middleName");
        Map<String, String> dateOfBirth = (Map<String, String>) individual.get("birthDate");

        this.title = unbox(title, "value");
        this.firstName = unbox(firstName, "value");
        this.lastName = unbox(lastName, "value");
        this.middleName = unbox(middleName, "value");
        this.dateOfBirth = LocalDate.parse(unbox(dateOfBirth, "value"));
    }
}

This is the sample util method – unbox – which I’ve created in order to avoid writing such ugly code. Right now, it only works for cases where String is returned.

import java.util.Map;

public class BaseUtil {
    // TODO: Make this method generic
    public static String unbox(Map<String, String> data, String key) {
        if (data != null && data.containsKey(key)) {
            return data.get(key);
        }
        return null;
    }
}

I’m trying to convert above method into a generic one where I could specify the return type dynamically and cast the returned data accordingly.

Can anyone help me out in creating one?

I’ve tried this:

public static <T> T unbox(Map<String, String> data, String key, Class<T> type) {
        if (data != null && data.containsKey(key)) {
            return (type) data.get(key);
        }
        return null;
    }

But it obviously doesn’t work but in theory that’s the kind of solution that I’m expecting.

EDIT: Here’s a sample input of complex type:

// The associatePartyRole is a list of Stings.
@JsonProperty(value = "associatedPartyRole")
public void unboxAssociatedPartyRole(Map<String, Object> data) {
    this.associatedPartyRole = unbox(data, "value", List.class); 

    // Compilation error: Need list, provided object.
}

EDIT 2: Here’s the final solution: PartyDetailsDto.java

public class PartyDetailsDto implements Serializable {
    private static final long serialVersionUID = 3851075484507637508L;

    private String partyId;
    private String partyType;
    private String title;
    private String firstName;
    private String lastName;
    private String middleName;
    private LocalDate dateOfBirth;

    @JsonProperty(value = "partyId")
    public void unboxPartyId(Map<String, String> data) {
        this.partyId = unbox(data, "evidenceId");
    }

    @JsonProperty(value = "partyType")
    public void unboxPartyType(Map<String, String> partyType) {
        this.partyType = unbox(partyType, "value");
    }

    @JsonProperty(value = "individual")
    public void unboxIndividualDetails(Map<String, Object> individual) {
        this.title = unbox(unbox(individual, "title", Map.class), "value");
        this.firstName = unbox(unbox(individual, "firstName", Map.class), "value");
        this.lastName = unbox(unbox(individual, "lastName", Map.class), "value");
        this.middleName = unbox(unbox(individual, "middleName", Map.class), "value");
        this.dateOfBirth = LocalDate.parse(unbox(unbox(individual, "title", Map.class), "value"));
    }
}

BaseUtil.java

public class BaseLineUtil {
    public static <T> T unbox(Map<String, Object> data, String key, Class<?> ofType) {
        return Optional.ofNullable(data)
                .map(m -> (T) ofType.cast(m.get(key)))
                .orElse(null);
    }

    public static <T> T unbox(Map<String, T> data, String key) {
        return Optional.ofNullable(data)
                .map(m -> (T) m.get(key))
                .orElse(null);
    }
}

Thanks @deduper @davidxxx for your answers.

Answer

…I followed [@davidxx’s] solution but it doesn’t seem to work when the return type is supposed to be a list or an array. How would i handle that case?…

Through a process I call „EDD“ („Experiment-driven Development“) the following way to handle those cases emerged…

public static < T > T unbox( Map< String, T > data, String key, Class< ? > ofType ) {
    if ( data != null && data.containsKey( key ) ) {
        return (T)ofType.cast( data.get( key ) ) ;
    }
    return null;
}

You can observe in main(String[]) that the following calls successfully return the expected result…

...
List< String > unBoxedList = unbox( mapOfLists, foo, List.class );
...
List< ? >[ ] unBoxedArrayOfLists = unbox( mapOfArrayOfLists, "bar", List[ ].class );
... 
String unBoxedString = unbox( mapOfStrings, foo, String.class );
...
Integer unBoxedInteger = unbox( mapOfIntegers, bar, Integer.class );
...

Click the green Start button at the top of the page in the link above, to run the experiment.



After feedback in the comments from @saran3h that clarified his use case, the following refactor emerged from a subsequent iteration of the experiment

public class BaseUtil {
    
    public List<Object> associatedPartyRole ;
    
    // TODO: Make this method generic
    public static < T > T unbox( Map< String, T > data, String key, Class< ? > ofType ) {
        if ( data != null && data.containsKey( key ) ) {
            return (T)ofType.cast( data.get( key ) ) ;
        }
        return null;
    }
    
    public void unboxAssociatedPartyRole(Map<String, Object> data) {
        this.associatedPartyRole = (List)unbox(data, "foo", Object.class);        
    }
}

That new case was successfully tested with…

...
private static final Map<String, Object> mapOfObjects = new HashMap< >( ); 
...
mapOfObjects.put( foo, (Object)mapOfLists.get( foo ) );
...
BaseUtil user = new BaseUtil( );

user.unboxAssociatedPartyRole( mapOfObjects );

List< Object > objs = user.associatedPartyRole;

assertIsA( objs, List.class );

Observe the results of running the experiment with the above refactor (pardon my French)…

[What The Reifiable Fuck@&*%$*!?]
                     EXPERIMENT SUCCESSFUL


Source: stackoverflow