Java generics: Map nested json response to Java objects

我是研究僧i 提交于 2021-01-28 11:14:34

问题


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.


回答1:


…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



回答2:


Maybe that :

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

Here T implies T extends Object.

That you can use so with any class:

Map<String, Integer> map = ...;
Integer value = unbox(map, "key");

Note that you could simplify your implementation such as :

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

It is also more efficient (a single map access)

OP comment :

I followed your 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

That is surprising. It should work. Try that sample code :

  public static void main(String[] args) {
    Map<String, Integer> map = new HashMap<>();
    map.put("key", 100);
    Integer value = unbox(map, "key");
    System.out.println(value);

    Map<String, List<Integer>> mapOfList = new HashMap<>();
    mapOfList.put("key", Arrays.asList(1, 2));
    List<Integer> valueList = unbox(mapOfList, "key");
    System.out.println(valueList);

    Map<String, int[]> mapOfArray = new HashMap<>();
    mapOfArray.put("key", new int[] { 1, 2, 3 });
    int[] valueArray = unbox(mapOfArray, "key");
    System.out.println(Arrays.toString(valueArray));
  }

It outputs :

100

[1, 2]

[1, 2, 3]

If that is not what you are looking for. Please rewrite your question by specifying exactly what you want to perform.


Edit after requiement change :

public void unboxIndividualDetails(Map<String, Object> individual) {...}

In fact here you want to perform unsafe casts. To achieve that you don't need to pass a Class instance and that not will not make your code more safe type either.
What you want is telling to the compiler to accept that the object that is declared as Object be assigned to a more specific type variable.
In terms of cast logic that looks like :

Object o = Integer.valueOf(100);
Integer i = (Integer)o;

By declaring a parameterized generic type method, the compiler infers T from the target type (the variable type that received the call method return), so you can do just return (T)myObject.

Concrete code :

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

And here a sample test :

public static void main(String[] args) {
    Map<String, Object> mapOfObjects = new HashMap< >( );
    mapOfObjects.put("longKey", 1L );
    mapOfObjects.put("stringKey", "hello" );
    Long l = unboxUnsafe(mapOfObjects, "longKey");
    System.out.println(l);
    String s = unboxUnsafe(mapOfObjects, "stringKey");
    System.out.println(s);
}

Output :

1

hello



来源:https://stackoverflow.com/questions/63424304/java-generics-map-nested-json-response-to-java-objects

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!