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á.