Temos a seguinte API de lista de tarefas feita com JAX-RS:
@Path("/tarefas")
public class TarefasResource {
@Inject
private TarefasRepository repo;
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response nova(Tarefa t) throws URISyntaxException {
repo.cria(t);
return Response.created(new URI("/tarefas/" + t.getId()))
.entity(t)
.build();
}
@GET
@Path("/{id}")
@Produces(MediaType.APPLICATION_JSON)
public Tarefa lista(@PathParam("id") Integer id) {
return repo.busca(id);
}
}
Uma tarefa tem as propriedades id, do tipo Integer descricao, do tipo String e data, do tipo LocalDate do pacote java.time:
public class Tarefa {
private Integer id;
private LocalDate data;
private String descricao;
//getters e setters...
}
A API de estilo REST define o recurso /tarefas. Para criar uma nova tarefa deve ser enviado um POST para a URI /tarefas com uma representação da tarefa em JSON. Para obter um JSON com a tarefa de id 1, deve ser enviado um GET para a URI /tarefas/1.
O código acima foi implantado em um Wildfly 8.2.0.Final.
Problemas ao serializar objetos do java.time de/para JSON
Mas há algo de estranho.
Ao enviarmos um GET para /tarefas/1, obtemos o seguinte JSON:
{
"id": 1,
"descricao": "Configurar JAX-RS",
"data": {"year":2016,"month":"FEBRUARY","chronology":{"calendarType":"iso8601","id":"ISO"},"era":"CE","dayOfYear":56,"dayOfWeek":"THURSDAY","leapYear":true,"dayOfMonth":25,"monthValue":2}
}
A representação do LocalDate em JSON ficou gigantesca! Vários detalhes internos foram exibidos…
E se tentarmos enviar um POST para /tarefas com o JSON abaixo?
{
"data": "2016-02-26",
"descricao": "Configurar Wildfly"
}
O resultado será um erro 400 (Bad Request) com a seguinte mensagem:
com.fasterxml.jackson.databind.JsonMappingException: Can not instantiate value of type [simple type, class java.time.LocalDate] from String value ('2016-02-26'); no single-String constructor/factory method
at [Source: io.undertow.servlet.spec.ServletInputStreamImpl@1c6dc29c; line: 1, column: 2] (through reference chain: br.com.alexandreaquiles.modelo.Tarefa["data"])
A mensagem de erro acima informa que o Jackson, a biblioteca de serialização JSON usada pelo Wildfly, não conseguiu transformar a String em um LocalDate.
Como fazer para ensinar o Jackson a trabalhar com um objeto do tipo LocalDate ou de outras classes do pacote java.time como se fossem Strings?
Melhorando a (de)serialização
O Wildfly 8.2.0.Final usa a versão 2.4.1 do Jackson, que tem a extensão jackson-datatype-jsr310, responsável por serializações mais interessantes de/para classes do pacote java.time.
Se estivermos usando o Maven, basta adicionar mais uma dependência:
<dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> <version>2.4.1</version> </dependency>
Outras ferramentas de gerenciamento de dependência teriam configurações análogas. Caso não esteja usando nenhuma ferramenta do tipo, baixe o jar.
Ao testarmos novamente, os mesmos erros acontecem… Que chato!
É que precisamos configurar um ContextResolver, anotando-o com @Provider e registrando o módulo JSR310Module do Jackson:
@Provider
public class JacksonJavaTimeConfiguration implements ContextResolver<ObjectMapper> {
private final ObjectMapper mapper;
public JacksonJavaTimeConfiguration() {
mapper = new ObjectMapper();
mapper.registerModule(new JSR310Module());
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
}
@Override
public ObjectMapper getContext(Class<?> type) {
return mapper;
}
}
Note a configuração WRITE_DATES_AS_TIMESTAMPS setada para false. Se não fizermos isso, as datas são representadas como um array no JSON, ao invés de um texto.
Agora, se enviarmos um GET para /tarefas/1, obtemos:
{
"id": 1,
"descricao": "Configurar JAX-RS",
"data": "2016-02-25";
}
O LocalDate passa a ser representado no JSON como uma String no formato ISO-8601.
Ao enviarmos o POST para /tarefas novamente, tudo dá certo! Recebemos um status 201 (Created).
Funciona em outro servidor de aplicação?
Infelizmente, não!
A grande questão é que nossa configuração foi feita para a biblioteca usada pelo Wildfly, o Jackson, inclusive em uma versão específica.
O Glassfish, por exemplo, usa a biblioteca MOXy para serialização de/para JSON. As configurações seriam diferentes…
Uma forma mais portável de resolver isso seria criar uma classe wrapper com uma String no construtor, como exemplificado aqui: https://docs.oracle.com/cd/E19798-01/821-1841/gipyw/index.html
Isso funciona pelo menos no Jersey (https://jersey.java.net/documentation/latest/jaxrs-resources.html#d0e2193) e no RestEasy (http://docs.jboss.org/resteasy/docs/3.0.13.Final/userguide/html/_PathParam.html).
Feio, mas funciona.
Luiz,
Eu até tentei fazer um ParamConverter para transformar Tarefa em String e vice-versa. Só que eu receberia um JSON como String e que usar alguma biblioteca de parsing de JSON p/ Objetos (ou fazer na mão). No final das contas, acabaria tendo que usar o Jackson ou algo parecido. E funcionaria só para Tarefa. Feio, como você falou… 🙂
Talvez haja alguma solução melhor mas não estou enxergando…
Infelizmente não tem outra melhor. Na verdade a sua solução é muito boa, só não é portável. A que eu sugeri é mais portável, mas feia e nem sempre possível, como parece no seu caso. Pra mim a solução é usar Spring e não JEE. 😛
Bom dia, Alexandre!
Aqui não funcionou, mesmo seguindo as suas instruções. Continuo recebendo o LocalDateTime como um JSON com os atributos month, year, hour…
Adicionei a dependência no pom.xml e criei o ContextResolver exatamente igual ao seu.
Preciso fazer mais alguma coisa para “registrar” o ContextResolver? Não sei onde está o problema..
Estou usando Wildlfy 10.
Obrigado.
Lucas,
Desculpe pela demora. Só tive tempo para dar uma olhada hoje…
Creio que o erro seja a versão do Jackson.
O Wildfly 10.0.0.Final usa a versão 2.5.4 do Jackson, o que pode ser observado em: https://github.com/wildfly/wildfly/blob/10.0.0.Final/pom.xml
Testei aqui e deu tudo certo!