Today in the packaging of third-party applications open interface, write a lot of return value class, many of these classes are similar in structure only individual field names are not the same. In order to separate the fields to copy a change is not to win, and the name is the most headache. Like the following two.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@EqualsAndHashCode(callSuper = true)
@Data
public class SimpleUserResponse extends WeComResponse {
    private List<SimpleUser> userlist;
}

@EqualsAndHashCode(callSuper = true)
@Data
public class UserDetailResponse extends WeComResponse {
    private List<UserDetail> userlist;
}

Is it similar? So they were merged using generics.

1
2
3
4
5
@EqualsAndHashCode(callSuper = true)
@Data
public class UserResponse<T> extends WeComResponse {
    private List<T> userlist;
}

This way they can be defined via UserResponse<SimpleUser> and UserResponse<UserDetail>, simplifying the code quite a bit. But it didn’t take long for another class to come along.

1
2
3
4
5
@EqualsAndHashCode(callSuper = true)
@Data
public class QrCodeResponse extends WeComResponse {
    private String qrcode;
}

This structure is actually pretty much the same, if you take UserResponse<T> and transform it further into.

1
2
3
4
5
@EqualsAndHashCode(callSuper = true)
@Data
public class OjbectResponse<T> extends WeComResponse {
    private T userlist;
}

It seems that OjbectResponse<String> is equivalent to QrCodeResponse. However, it is not possible to simply do this, as careful students will notice that the names of their properties are different, one is qrcode and the other is userlist. It would be nice if they had different names! I seem to have a solution.

If it’s a type conversion

If it’s a Bean type conversion, using Mapstruct solves the problem and we end up defining the property name as data.

1
2
    @Mapping(target = "data", source = "qrcode")
    @Mapping(target = "data", source = "userlist")

This is solved by writing two conversion interfaces with the two annotated maps above. For Mapstruct you can see my related article explaining it.

If it’s deserialization

Jackson provides an alias annotation @JsonAlias that allows field attribute names to accept more aliases. Like this.

1
2
3
4
5
6
@EqualsAndHashCode(callSuper = true)
@Data
public class OjbectResponse<T> extends WeComResponse {
    @JsonAlias({"qrcode","userlist"})
    private T data;
}

Then the following json can be mapped to OjbectResponse<String>.

1
2
3
{
    "qrcode":"https://felord.cn/myqr.png"
}

This will map to OjbectResponse<List<UserDetail>>.

1
2
3
{
 "userlist":[{"username":"felord.cn"},{"username":"felordcn"},{"username":"felord"}]
}

By this point you may have questions: How does Jackson handle generics?

How to get the Class type of a generic type

You can’t get the Class type of a generic type by direct means, but we can get the abstract definition of a generic type java.lang.reflect.ParameterizedType, and it’s not very convenient to use ParameterizedType directly. So in Jackson you can handle generics with TypeReference<T>. If we need to deserialize OjbectResponse<String> we can do so.

1
2
3
ObjectMapper objectMapper = new ObjectMapper();
String json = "{\"qrcode\":\"https://felord.cn/myqr.png\"}";
OjbectResponse<String> obj = objectMapper.readValue(json,new TypeReference<OjbectResponse<String>>(){});

Spring actually provides a similar tool class org.springframework.core.ParameterizedTypeReference<T>, especially if you are using RestTemplate to request third-party generic processing tools.