Utilisation de facultatif avec Jackson
1. introduction
Dans cet article, nous allons donner un aperçu de la classeOptional, puis expliquer certains problèmes que nous pourrions rencontrer lors de son utilisation avec Jackson.
Ensuite, nous allons introduire une solution qui permettra à Jackson de traiter lesOptionals comme s’il s’agissait d’objets Nullables ordinaires.
2. Aperçu du problème
Tout d'abord, voyons ce qui se passe lorsque nous essayons de sérialiser et de désérialiserOptionals avec Jackson.
2.1. Dépendance Maven
Pour utiliser Jackson, vérifions que nous utilisonsits latest version:
com.fasterxml.jackson.core
jackson-core
2.9.6
2.2. Notre livre objet
Ensuite, créons une classeBook, contenant un champ ordinaire et un champOptional:
public class Book {
String title;
Optional subTitle;
// getters and setters omitted
}
Gardez à l'esprit queOptionals ne doit pas être utilisé comme champs et nous faisons cela pour illustrer le problème.
2.3. La sérialisation
Maintenant, instancions unBook:
Book book = new Book();
book.setTitle("Oliver Twist");
book.setSubTitle(Optional.of("The Parish Boy's Progress"));
Et enfin, essayons de le sérialiser à l'aide d'un JacksonObjectMapper:
String result = mapper.writeValueAsString(book);
Nous verrons que la sortie du champOptional, ne contient pas sa valeur, mais plutôt un objet JSON imbriqué avec un champ appelépresent:
{"title":"Oliver Twist","subTitle":{"present":true}}
Bien que cela puisse paraître étrange, c'est en fait ce à quoi nous devons nous attendre.
Dans ce cas,isPresent() est un getter public sur la classeOptional. Cela signifie qu'il sera sérialisé avec une valeur detrue oufalse, selon qu'il est vide ou non. Il s'agit du comportement de sérialisation par défaut de Jackson.
Si nous y réfléchissons, ce que nous voulons, c'est que la valeur réelle du champsubtitle soit sérialisée.
2.4. Désérialisation
Maintenant, inversons notre exemple précédent, cette fois en essayant de désérialiser un objet en unOptional. Nous allons voir que maintenant nous obtenons unJsonMappingException:
@Test(expected = JsonMappingException.class)
public void givenFieldWithValue_whenDeserializing_thenThrowException
String bookJson = "{ \"title\": \"Oliver Twist\", \"subTitle\": \"foo\" }";
Book result = mapper.readValue(bookJson, Book.class);
}
Voyons la trace de la pile:
com.fasterxml.jackson.databind.JsonMappingException:
Can not construct instance of java.util.Optional:
no String-argument constructor/factory method to deserialize from String value ('The Parish Boy's Progress')
Ce comportement a encore un sens. Essentiellement, Jackson a besoin d'un constructeur qui peut prendre la valeur desubtitle comme argument. Ce n'est pas le cas avec notre champOptional.
3. Solution
Ce que nous voulons, c'est que Jackson traite unOptional vide commenull, et traite unOptional présent comme un champ représentant sa valeur.
Heureusement, ce problème a été résolu pour nous. Jackson has a set of modules that deal with JDK 8 datatypes, y comprisOptional.
3.1. Dépendance et enregistrement Maven
Tout d'abord, ajoutons la dernière version en tant que dépendance Maven:
com.fasterxml.jackson.datatype
jackson-datatype-jdk8
2.9.6
Maintenant, il ne nous reste plus qu'à enregistrer le module avec nosObjectMapper:
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new Jdk8Module());
3.2. La sérialisation
Maintenant, testons-le. Si nous essayons de sérialiser à nouveau notre objetBook, nous verrons qu'il existe maintenant unsubtitle, par opposition à un JSON imbriqué:
Book book = new Book();
book.setTitle("Oliver Twist");
book.setSubTitle(Optional.of("The Parish Boy's Progress"));
String serializedBook = mapper.writeValueAsString(book);
assertThat(from(serializedBook).getString("subTitle"))
.isEqualTo("The Parish Boy's Progress");
Si nous essayons de sérialiser un livre vide, il sera stocké en tant quenull:
book.setSubTitle(Optional.empty());
String serializedBook = mapper.writeValueAsString(book);
assertThat(from(serializedBook).getString("subTitle")).isNull();
3.3. Désérialisation
Maintenant, répétons nos tests de désérialisation. Si nous relisons notre livre, nous verrons que nous n’obtenons plus deJsonMappingException:
Book newBook = mapper.readValue(result, Book.class);
assertThat(newBook.getSubTitle()).isEqualTo(Optional.of("The Parish Boy's Progress"));
Enfin, répétons le test, cette fois avecnull. Nous verrons qu’une fois de plus nous n’obtenons pas deJsonMappingException, et en fait, nous avons unOptional: vide
assertThat(newBook.getSubTitle()).isEqualTo(Optional.empty());
4. Conclusion
Nous avons montré comment contourner ce problème en tirant parti du module JDK 8 DataTypes, en démontrant comment il permet à Jackson de traiter unOptional vide commenull, et un présentOptional comme un champ ordinaire .
L'implémentation de ces exemples peut être trouvéeover on GitHub; il s'agit d'un projet basé sur Maven, il devrait donc être facile à exécuter tel quel.