Usando opcional com Jackson

Usando opcional com Jackson

 

1. Introdução

Neste artigo, daremos uma visão geral da classeOptional e, em seguida, explicaremos alguns problemas que podemos encontrar ao usá-la com Jackson.

Em seguida, apresentaremos uma solução que fará com que Jackson trateOptionals como se fossem objetos anuláveis ​​comuns.

2. Visão geral do problema

Primeiro, vamos dar uma olhada no que acontece quando tentamos serializar e desserializarOptionals com Jackson.

2.1. Dependência do Maven

Para usar Jackson, vamos ter certeza de que estamos usandoits latest version:


    com.fasterxml.jackson.core
    jackson-core
    2.9.6

2.2. Nosso objeto de livro

Então, vamos criar uma classeBook, contendo um campo comum e umOptional:

public class Book {
   String title;
   Optional subTitle;

   // getters and setters omitted
}

Lembre-se de queOptionals não deve ser usado como campos e estamos fazendo isso para ilustrar o problema.

2.3. Serialização

Agora, vamos instanciar umBook:

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

E, finalmente, vamos tentar serializá-lo usando JacksonObjectMapper:

String result = mapper.writeValueAsString(book);

Veremos que a saída do campoOptional não contém seu valor, mas sim um objeto JSON aninhado com um campo chamadopresent:

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

Embora possa parecer estranho, é realmente o que devemos esperar.

Nesse caso,isPresent() é um getter público na classeOptional. Isso significa que será serializado com um valor detrue oufalse, dependendo se está vazio ou não. Este é o comportamento de serialização padrão de Jackson.

Se pensarmos bem, o que queremos é de fato o valor do camposubtitle a ser serializado.

2.4. Desserialização

Agora, vamos reverter nosso exemplo anterior, desta vez tentando desserializar um objeto em umOptional.. Veremos que agora temos umJsonMappingException:

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

Vamos ver o rastreamento da pilha:

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

Esse comportamento novamente faz sentido. Essencialmente, Jackson precisa de um construtor que possa tomar o valor desubtitle como argumento. Este não é o caso com nosso campoOptional.

3. Solução

O que queremos, é que Jackson trate umOptional vazio comonull,e trate umOptional presente como um campo que representa seu valor.

Felizmente, esse problema foi resolvido para nós. Jackson has a set of modules that deal with JDK 8 datatypes, incluindoOptional.

3.1. Dependência e registro do Maven

Primeiro, vamos adicionar a versão mais recente como uma dependência Maven:


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

Agora, tudo o que precisamos fazer é registrar o módulo com nossoObjectMapper:

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

3.2. Serialização

Agora, vamos testar. Se tentarmos serializar nosso objetoBook novamente, veremos que agora há umsubtitle, em oposição a um JSON aninhado:

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 tentarmos serializar um livro vazio, ele será armazenado comonull:

book.setSubTitle(Optional.empty());
String serializedBook = mapper.writeValueAsString(book);

assertThat(from(serializedBook).getString("subTitle")).isNull();

3.3. Desserialização

Agora, vamos repetir nossos testes de desserialização. Se relermos nosso livro, veremos que não obteremos mais umJsonMappingException:

Book newBook = mapper.readValue(result, Book.class);

assertThat(newBook.getSubTitle()).isEqualTo(Optional.of("The Parish Boy's Progress"));

Finalmente, vamos repetir o teste novamente, desta vez comnull. Veremos que mais uma vez não obtemos umJsonMappingException,e, na verdade, temos umOptional: vazio

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

4. Conclusão

Mostramos como contornar esse problema aproveitando o módulo JDK 8 DataTypes, demonstrando como ele permite que Jackson trate umOptional vazio comonull,e umOptional presente como um campo comum .

A implementação desses exemplos pode ser encontradaover on GitHub; este é um projeto baseado em Maven, portanto, deve ser fácil de executar como está.