web-dev-qa-db-fra.com

Jackson est-il vraiment incapable de désérialiser JSON en un type générique?

Il s'agit d'une question en double, car les questions suivantes sont soit compliquées soit sans réponse:

deserializing-a-generic-type-with-jackson

jackson-deserialize-in-runtime-spécifiée-classe

jackson-deserialize-using-generic-class

variable-classe-générique-jackson

J'espère que cette question trouvera enfin une réponse qui le rendra clair pour de bon.

Avoir un modèle:

public class AgentResponse<T> {

    private T result;

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

Entrée JSON:

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

et deux méthodes recommandées pour la désérialisation des types génériques:

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

ou

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

Jackson n'est jamais capable de traiter le type générique T, il suppose qu'il s'agit d'un mappage de JavaType, mais il trouve l'argument du constructeur du type Object en raison de l'effacement du type et génère une erreur. Alors est-ce un bug de Jackson, ou est-ce que je fais quelque chose de mal? Quoi d'autre est spécification explicite de TypeReference ou JavaType pour?

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)
41
lisak

Vous devez ajouter des annotations sur le constructeur pour indiquer à Jackson comment construire l'objet. Ce qui suit a fonctionné pour moi:

public class AgentResponse<T> {

    private T result;

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

Sans l'annotation @JsonCreator, Jackson ne peut pas appeler ce constructeur. Et sans l'annotation @JsonProperty, Jackson ne sait pas que le premier argument du constructeur est mappé sur la propriété result.

53
cambecc

J'ai essayé d'utiliser la même approche, mais je n'ai pas annoté ma classe de modèles. Cela a bien fonctionné pour moi.

Ceci est ma classe de modèle

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;
    }
}

Et j'ai utilisé la méthode suivante pour désérialiser la charge utile

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) {

        }
 }

où jsonString contient l'objet BasicMessageObject dans une chaîne.

6
yaswanth

La chaîne JSON qui doit être désérialisée devra contenir les informations de type relatives au paramètre T.
Vous devrez mettre des annotations de Jackson sur chaque classe pouvant être passée en tant que paramètre T à class AgentResponse afin que les informations de type relatives au paramètre type T puissent être lues/écrites dans la chaîne JSON par Jackson.

Supposons que T puisse être n'importe quelle classe qui étend la classe abstraite 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 {

}

Une fois que chaque classe (ou son supertype commun) pouvant être passée en tant que paramètre T est annotée, Jackson inclura des informations sur le paramètre T dans le JSON. Ce JSON peut ensuite être désérialisé sans connaître le paramètre T au moment de la compilation.
Ce lien dans la documentation Jackson parle de la désérialisation polymorphe, mais il est utile de s'y référer également.

1
Sanjeev Sachdev
public class AgentResponse<T> {

private T result;

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

}

Donc, pour la structure de classe ci-dessus et T peut être de type T1, classe T2

Donc, pour désérialiser AgentResponse, utilisez le code suivant 

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