简体   繁体   中英

Jackson Decoding Generic Types

I am building a client/server application, and am encapsulating the request and response messages in generic Java classes that looks like this:

@SuperBuilder
@Jacksonized
@Getter
@Setter
public class AppResponse<T extends AppResponse.Results> {
    public static final String SUCCESS_ATTRIBUTE = "success";
    public static final String MESSAGE_ATTRIBUTE = "message";
    public static final String REQUEST_ID_ATTRIBUTE = AppRequest.REQUEST_ID_ATTRIBUTE;
    public static final String RESULTS_ATTRIBUTE = "results";

    @JsonProperty(SUCCESS_ATTRIBUTE)
    boolean success;

    @JsonProperty(MESSAGE_ATTRIBUTE)
    String message;

    @JsonProperty(REQUEST_ID_ATTRIBUTE)
    String requestID;

    @JsonProperty(RESULTS_ATTRIBUTE)
    T results;

    @SuperBuilder
    @Jacksonized
    @Getter
    @Setter
    public static class Results {

    }
}

Then I have one class that extends AppResponse.Results for each command response packet that can be sent over the wire. For example:

@SuperBuilder
@Getter
@Setter
@Jacksonized
public class AppCreateForumPostResults extends AppResponse.Results {
    @JsonProperty("id")
    UUID id;
}

So when the server encodes this to JSON using Jackson, I get a JSON document that looks like this:

{
  "success" : true,
  "requestID" : "eL55P4Sz",
  "results" : {
    "id" : "0b48d389-2407-43c4-ad1b-ab52599c2d7f"
  }
}

When the client application receives this message, it first decodes it to an ObjectNode:

objectNode = (ObjectNode) AppObjectMapper.OBJECT_MAPPER.readTree(message);

It then decodes that into a generic object, because it does not yet know what the concrete type for the generic is yet:

JavaType genericType = AppObjectMapper.OBJECT_MAPPER.getTypeFactory().constructParametricType(
    AppResponse.class,
    AppResponse.Results.class
);

AppResponse<AppResponse.Results> genericResponse = AppObjectMapper.OBJECT_MAPPER.treeToValue(
    objectNode,
    genericType
);

Then it consults a HashMap of requestIDs that have been sent to find out what kind of results class it should expect. The HashMap is defined as:

private final Map<String, CallbackEntry<? extends AppResponse.Results>> callbacks = new HashMap<>();

And CallbackEntry is defined as:

@Builder
@Value
public static class CallbackEntry<T extends AppResponse.Results> {
    Callback<T> method;
    Class<T> resultsClass;
}

And finally Callback is defined as:

public interface Callback<T extends AppResponse.Results> {
    void handleResponse(AppResponse<T> results);
}

So once we have found the requestID in the HashMap, we know what concrete class the results contain, and we can decode it:

CallbackEntry<? extends AppResponse.Results> callbackEntry = callbacks.get(genericResponse.getRequestID());

callbacks.remove(genericResponse.getRequestID());

JavaType concreteType = AppObjectMapper.OBJECT_MAPPER.getTypeFactory().constructParametricType(
    AppResponse.class,
    callbackEntry.resultsClass
);

AppResponse<? extends AppResponse.Results> concreteResponse = AppObjectMapper.OBJECT_MAPPER.treeToValue(
    objectNode,
    concreteType
);

So far, so good. This all works exactly as expected. If I print out the concreteResponse object, it has exactly the correct data in it. However, when I go to actually call the callback method, I'm getting an error. The call looks like:

callbackEntry.method.handleResponse(concreteResponse);

And the error I get on that line of code is:

[127,49] incompatible types: com.whatever.app.common.AppResponse<capture#1 of ? extends com.whatever.app.common.AppResponse.Results> cannot be converted to com.whatever.app.common.AppResponse<capture#2 of ? extends com.whatever.app.common.AppResponse.Results>

I'm a bit confused by the error message. I'm guessing it has to do somehow with the way in which the generics are being handled, but I just can't figure out what the correct incantation is to get this to go. I've done a bunch of Googling, and find similar errors here and there, but they generally are due to someone using? instead of T, which (I think) is not the case here.

So, what am I missing?

Any help is greatly appreciated!

I was able to get this to work by casting the function parameter like this:

callbackEntry.method.handleResponse(
    (concreteResponse.getClass()).cast(concreteResponse)
);

That shows a warning for an unchecked assignment, but it works.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM