web-dev-qa-db-fra.com

Comment créer un désérialiseur personnalisé à Jackson pour un type générique?

Imaginez le scénario suivant:

class <T> Foo<T> {
    ....
}

class Bar {
    Foo<Something> foo;
}

Je veux écrire un désérialiseur Jackson personnalisé pour Foo. Pour ce faire (par exemple, afin de désérialiser Bar classe qui a Foo<Something> propriété), j'ai besoin de connaître le type concret de Foo<T>, utilisé dans Bar, au moment de la désérialisation (par exemple, j'ai besoin de savoir que T est Something dans ce cas particulier).

Comment écrit-on un tel désérialiseur? Il devrait être possible de le faire, puisque Jackson le fait avec des collections et des cartes tapées.

Précisions:

Il semble qu'il y ait 2 parties pour résoudre le problème:

1) Obtenez le type de propriété déclaré foo dans Bar et utilisez-le pour désérialiser Foo<Somehting>

2) Découvrez au moment de la désérialisation que nous désérialisons la propriété foo à l'intérieur de la classe Bar afin de réussir l'étape 1)

Comment termine-t-on 1 et 2?

15
Kresimir Nesek

Vous pouvez implémenter un JsonDeserializer personnalisé pour votre type générique qui implémente également ContextualDeserializer .

Par exemple, supposons que nous ayons le type d'encapsuleur simple suivant qui contient une valeur générique:

public static class Wrapper<T> {
    public T value;
}

Nous voulons maintenant désérialiser JSON qui ressemble à ceci:

{
    "name": "Alice",
    "age": 37
}

dans une instance d'une classe qui ressemble à ceci:

public static class Person {
    public Wrapper<String> name;
    public Wrapper<Integer> age;
}

L'implémentation de ContextualDeserializer nous permet de créer un désérialiseur spécifique pour chaque champ de la classe Person, en fonction des paramètres de type générique du champ. Cela nous permet de désérialiser le nom sous forme de chaîne et l'âge sous forme d'entier.

Le désérialiseur complet ressemble à ceci:

public static class WrapperDeserializer extends JsonDeserializer<Wrapper<?>> implements ContextualDeserializer {
    private JavaType valueType;

    @Override
    public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
        JavaType wrapperType = property.getType();
        JavaType valueType = wrapperType.containedType(0);
        WrapperDeserializer deserializer = new WrapperDeserializer();
        deserializer.valueType = valueType;
        return deserializer;
    }

    @Override
    public Wrapper<?> deserialize(JsonParser parser, DeserializationContext ctxt) throws IOException {
        Wrapper<?> wrapper = new Wrapper<>();
        wrapper.value = ctxt.readValue(parser, valueType);
        return wrapper;
    }
}

Il est préférable de regarder createContextual ici d'abord, car cela sera appelé en premier par Jackson. Nous lisons le type du champ dans le BeanProperty (par exemple Wrapper<String>), puis extrayez le premier paramètre de type générique (par exemple String). Nous créons ensuite un nouveau désérialiseur et stockons le type interne en tant que valueType.

Une fois que deserialize est appelé sur ce désérialiseur nouvellement créé, nous pouvons simplement demander à Jackson de désérialiser la valeur en tant que type interne plutôt qu'en tant que type de wrapper complet, et renvoyer un nouveau Wrapper contenant la valeur désérialisée .

Pour enregistrer ce désérialiseur personnalisé, nous devons ensuite créer un module qui le contient et enregistrer ce module:

SimpleModule module = new SimpleModule()
        .addDeserializer(Wrapper.class, new WrapperDeserializer());

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(module);

Si nous essayons ensuite de désérialiser l'exemple JSON ci-dessus, nous pouvons voir qu'il fonctionne comme prévu:

Person person = objectMapper.readValue(json, Person.class);
System.out.println(person.name.value);  // prints Alice
System.out.println(person.age.value);   // prints 37

Il y a plus de détails sur le fonctionnement des désérialiseurs contextuels dans documentation Jackson .

35
andersschuller

Si la cible elle-même est un type générique, la propriété sera nulle, pour cela, vous devrez obtenir le valueTtype du DeserializationContext:

@Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
    if (property == null) { //  context is generic
        JMapToListParser parser = new JMapToListParser();
        parser.valueType = ctxt.getContextualType().containedType(0);
        return parser;
    } else {  //  property is generic
        JavaType wrapperType = property.getType();
        JavaType valueType = wrapperType.containedType(0);
        JMapToListParser parser = new JMapToListParser();
        parser.valueType = valueType;
        return parser;
    }
}
2
Ahmed Mkaouar