Utilizzo di Opzionale con Jackson

1. Introduzione

In questo articolo, daremo una panoramica della classe Opzionale , e poi spiegheremo alcuni problemi che potremmo incontrare quando la usiamo con Jackson.

Successivamente, introdurremo una soluzione che porterà Jackson a trattare gli Optionals come se fossero normali oggetti nullable.

2. Panoramica del problema

Per prima cosa, diamo un'occhiata a cosa succede quando proviamo a serializzare e deserializzare gli Optionals con Jackson.

2.1. Dipendenza da Maven

Per usare Jackson, assicuriamoci di utilizzare la sua ultima versione:

 com.fasterxml.jackson.core jackson-core 2.11.1 

2.2. Il nostro oggetto libro

Quindi, creiamo un libro di classe , contenente un campo ordinario e uno opzionale :

public class Book { String title; Optional subTitle; // getters and setters omitted }

Tieni presente che gli Optionals non dovrebbero essere usati come campi e lo stiamo facendo per illustrare il problema.

2.3. Serializzazione

Ora, istanziamo un libro :

Book book = new Book(); book.setTitle("Oliver Twist"); book.setSubTitle(Optional.of("The Parish Boy's Progress"));

E infine, proviamo a serializzarlo usando un Jackson ObjectMapper :

String result = mapper.writeValueAsString(book);

Vedremo che l'output del campo Optional , non contiene il suo valore, ma invece un oggetto JSON annidato con un campo chiamato present :

{"title":"Oliver Twist","subTitle":{"present":true}}

Sebbene possa sembrare strano, in realtà è quello che dovremmo aspettarci.

In questo caso, isPresent () è un getter pubblico sulla classe opzionale . Ciò significa che verrà serializzato con un valore true o false , a seconda che sia vuoto o meno. Questo è il comportamento di serializzazione predefinito di Jackson.

Se ci pensiamo, quello che vogliamo è che il valore effettivo del campo dei sottotitoli venga serializzato.

2.4. Deserializzazione

Ora, invertiamo il nostro esempio precedente, questa volta provando a deserializzare un oggetto in un Opzionale. Vedremo che ora otteniamo un'eccezione JsonMappingException:

@Test(expected = JsonMappingException.class) public void givenFieldWithValue_whenDeserializing_thenThrowException String bookJson = "{ \"title\": \"Oliver Twist\", \"subTitle\": \"foo\" }"; Book result = mapper.readValue(bookJson, Book.class); } 

Vediamo la traccia dello stack:

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

Anche questo comportamento ha senso. In sostanza, Jackson ha bisogno di un costruttore che possa prendere il valore del sottotitolo come argomento. Questo non è il caso del nostro campo opzionale .

3. Soluzione

Quello che vogliamo è che Jackson tratti un Opzionale vuoto come nullo e tratti un Opzionale presente come un campo che ne rappresenta il valore.

Fortunatamente, questo problema è stato risolto per noi. Jackson ha una serie di moduli che si occupano dei tipi di dati JDK 8, incluso Opzionale .

3.1. Dipendenza e registrazione di Maven

Innanzitutto, aggiungiamo l'ultima versione come dipendenza Maven:

 com.fasterxml.jackson.datatype jackson-datatype-jdk8 2.9.6 

Ora, tutto ciò che dobbiamo fare è registrare il modulo con il nostro ObjectMapper :

ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(new Jdk8Module());

3.2. Serializzazione

Ora, proviamolo. Se proviamo di nuovo a serializzare il nostro oggetto Book , vedremo che ora c'è un sottotitolo, al contrario di un JSON annidato:

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");

Se proviamo a serializzare un libro vuoto, verrà memorizzato come null :

book.setSubTitle(Optional.empty()); String serializedBook = mapper.writeValueAsString(book); assertThat(from(serializedBook).getString("subTitle")).isNull();

3.3. Deserializzazione

Ora, ripetiamo i nostri test per la deserializzazione. Se rileggiamo il nostro libro, vedremo che non otteniamo più un'eccezione JsonMappingException:

Book newBook = mapper.readValue(result, Book.class); assertThat(newBook.getSubTitle()).isEqualTo(Optional.of("The Parish Boy's Progress"));

Infine, ripetiamo di nuovo il test, questa volta con null. Vedremo che ancora una volta non otteniamo una JsonMappingException e , in effetti, abbiamo un Opzionale vuoto :

assertThat(newBook.getSubTitle()).isEqualTo(Optional.empty());

4. Conclusione

Abbiamo mostrato come aggirare questo problema sfruttando il modulo JDK 8 DataTypes, dimostrando come consente a Jackson di trattare un Opzionale vuoto come nullo e un Opzionale presente come un campo ordinario.

L'implementazione di questi esempi può essere trovata su GitHub; questo è un progetto basato su Maven, quindi dovrebbe essere facile da eseguire così com'è.