Home > front end >  Jackson Decoding Generic Types
Jackson Decoding Generic Types

Time:02-28

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!

CodePudding user response:

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.

  • Related