Is Jackson really unable to deserialize json into

2019-01-07 10:29发布

问题:

This is a duplicate question because the following questions are either messy or they are not answered at all:

deserializing-a-generic-type-with-jackson

jackson-deserialize-into-runtime-specified-class

jackson-deserialize-using-generic-class

jackson-deserialize-generic-class-variable

I hope that this question will finally find an answer that makes this clear for good.

Having a model :

public class AgentResponse<T> {

    private T result;

    public AgentResponse(T result) {
        this.result = result;
    }
    public T getResult() {
        return result;
    }
}

JSON input:

{"result":{"first-client-id":3,"test-mail-module":3,"third-client-id":3,"second-client-id":3}}

and two recommended ways of deserializing generic types :

mapper.readValue(out, new TypeReference<AgentResponse<Map<String, Integer>>>() {}); 

or

JavaType javaType = mapper.getTypeFactory().constructParametricType(AgentResponse.class, Map.class);
mapper.readValue(out, javaType);

Jackson is never able to deal with the generic type T, it figures it's a Map from JavaType, but it finds Object type constructor argument because of type erasure and throws an error. So is this a Jackson bug, or am I doing something wrong? What else is explicit specification of TypeReference or JavaType for?

com.fasterxml.jackson.databind.JsonMappingException: No suitable constructor found for type [simple type, class com.fg.mail.smtp.AgentResponse<java.util.Map<java.lang.String,java.lang.Integer>>]: can not instantiate from JSON object (need to add/enable type information?)
at [Source: java.io.InputStreamReader@4f2d26d; line: 1, column: 2]
at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:164)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:984)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:276)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:121)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:2888)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2064)

回答1:

You need to add some annotations on the constructor to tell Jackson how to build the object. The following worked for me:

public class AgentResponse<T> {

    private T result;

    @JsonCreator
    public AgentResponse(@JsonProperty("result") T result) {
        this.result = result;
    }
    public T getResult() {
        return result;
    }
}

Without the @JsonCreator annotation, Jackson cannot know to call this constructor. And without the @JsonProperty annotation, Jackson does not know that the first argument of the constructor maps to the result property.



回答2:

I tried using the same approach but I haven't annotated my model class. It worked fine for me.

This is my model class

public class BasicMessage<T extends Serializable> implements Message<T> {
    private MessageHeader messageHeader = new MessageHeader();
    private T payload;
    public MessageHeader getHeaders() {
        return messageHeader;
    }

    public Object getHeader(String key) {
        return messageHeader.get(key);
    }

    public Object addHeader(String key, Object header) {
        return messageHeader.put(key, header);
    }

    public T getPayload() {
        return payload;
    }

    public void setPayload(T messageBody) {
        this.payload = messageBody;
    }
}

And I used the following method for deserializing the payload

public static <T extends Serializable> BasicMessage<T> getConcreteMessageType(String jsonString, Class<T> classType) {
        try {
            ObjectMapper mapper = new ObjectMapper();
            JavaType javaType = mapper.getTypeFactory().constructParametricType(BasicMessage.class, classType);
            return mapper.readValue(jsonString, javaType);
        } catch (IOException e) {

        }
 }

where jsonString contains the BasicMessageObject in a string.



回答3:

JSON string that needs to be deserialized will have to contain the type information about parameter T.
You will have to put Jackson annotations on every class that can be passed as parameter T to class AgentResponse so that the type information about parameter type T can be read from / written to JSON string by Jackson.

Let us assume that T can be any class that extends abstract class Result.

public class AgentResponse<T extends Result> {
    public Hits<T> hits;
}

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT)
@JsonSubTypes({
        @JsonSubTypes.Type(value = ImageResult.class, name = "ImageResult"),
        @JsonSubTypes.Type(value = NewsResult.class, name = "NewsResult")})
public abstract class Result {

}

public class ImageResult extends Result {

}

public class NewsResult extends Result {

}

Once each of the class (or their common supertype) that can be passed as parameter T is annotated, Jackson will include information about parameter T in the JSON. Such JSON can then be deserialized without knowing the parameter T at compile time.
This Jackson documentation link talks about Polymorphic Deserialization but is useful to refer to for this question as well.



回答4:

public class AgentResponse<T> {

private T result;

public AgentResponse(T result) {
    this.result = result;
}
public T getResult() {
    return result;
}

}

So for the above class structure and T can be of Type T1,T2 class

So to deserialize for AgentResponse, use the following code

JavaType javaType = objectMapper.getTypeFactory.constructParametricType(AgentResponse.class,T1.class)
AgentResponse<T1> agentResponseT1 = objectMapper.readValue(inputJson,javaType);