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