Jackson, Json e o tratamento adequado de atributos desconhecidos em API’s

Como não ser pego de surpresa pela exceção UnrecognizedPropertyException

Imagine o seguinte cenário: Você tem uma aplicação que se integra com outra através do consumo de endpoints REST. Para realizar a serialização/desserialização você utiliza a famosa biblioteca Jackson que transforma magicamente objetos java em json (serialização) e vice-versa (desserialização). Um belo dia, de forma totalmente repentina, suas requisições param de funcionar com uma exceção parecida com a abaixo:

com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field

O que pode ter acontecido? A própria exceção te diz:

  • UnrecognizedPropertyException: exceção de propriedade não reconhecida
  • Unrecognized field: campo não reconhecido

Specialized JsonMappingException sub-class specifically used to indicate problems due to encountering a JSON property that could not be mapped to an Object property (via getter, constructor argument or field).

De maneira sucinta sempre que existir no json uma propriedade que não foi mapeada em seu objeto java / DTO o Jackson lançará essa exceção.

Então o que pode ter acontecido?

O provedor do serviço que sua aplicação está consumindo adicionou um atributo novo no retorno do mesmo. Uma vez que tal atributo não existe em seu objeto java / DTO temos uma propriedade não reconhecida, impossibilitando o processo de desserialização (json -> objeto).

Ok Sherlock! E agora?

Parafraseando, e corrigindo, a mim mesmo: sempre que existir no json uma propriedade que não foi mapeada em seu objeto java / DTO o Jackson lançará essa exceção, exceto se você disser ao Jackson que ele pode ignorar tais atributos.

Ignorando campos desconhecidos com Jackson

Felizmente existe duas formas de contornar o problema em questão e evitar o lançamento da exceção:

  • Anotar a classe com @JsonIgnoreProperties(ignoreUnknown=true)
  • Configurar a Deserialization Feature FAIL_ON_UNKNOWN_PROPERTIES como false

@JsonIgnoreProperties(ignoreUnknown=true)

Adicionar a sua classe a anotação @JsonIgnoreProperties(ignoreUnknown=true) dirá ao Jackson para ignorar atributos desconhecidos ao realizar a desserialização de json's para objetos dessa classe.

@JsonIgnoreProperties(ignoreUnknown=true)
public class PessoaComAnotacaoDto {
  private String nome;
  private String sexo;
  // ...
}

Configurar a Deserialization Feature FAIL_ON_UNKNOWN_PROPERTIES como false

Configurar o object mapper dirá ao Jackson para ignorar atributos desconhecidos em todas as desserializações em que esse object mapper for utilizado.

// Versão 1.9 ou anterior
objectMapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);

// Versão 2.0 ou posterior
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

Vamos aos testes

Para nossos testes vamos precisar de dois DTO's, uma sem anotação (para comprovar o lançamento da exceção) e outro com anotação (para comprovar a resolução do problema).

public class PessoaSemAnotacaoDto {
  private String nome;
  private String sexo;
  // ...
}

@JsonIgnoreProperties(ignoreUnknown=true)
public class PessoaComAnotacaoDto {
  private String nome;
  private String sexo;
  // ...
}

Será utilizado o json abaixo, que possui o atributo idade que não é conhecido pelo DTO.

{ "nome": "LINUS" , "idade": 18, "sexo": "MASCULINO" }

Abaixo temos três testes:

@SpringBootTest
class JacksonIgnorePropertiesTests {

  private String JSON_PARA_DESSERIALIZAR = "{ \"nome\": \"LINUS\" , \"idade\": 18, \"sexo\": \"MASCULINO\" }";

    @Test
    void dadoJsonComAtributoDesconhecido_quandoSemAnotacaoOuConfiguracao_iraLancarException() throws JsonMappingException, JsonProcessingException {
      ObjectMapper mapper = new ObjectMapper();
      assertThrows(UnrecognizedPropertyException.class, () -> { mapper.readValue(JSON_PARA_DESSERIALIZAR, PessoaSemAnotacaoDto.class); });
    }

    @Test
    void dadoJsonComAtributoDesconhecido_quandoDtoPossuirAnotacaoJsonIgnoreProperties_iraDesserializar() throws JsonMappingException, JsonProcessingException {
      ObjectMapper mapper = new ObjectMapper();
      assertEquals("LINUS", mapper.readValue(JSON_PARA_DESSERIALIZAR, PessoaComAnotacaoDto.class).getNome());
    }

    @Test
    void dadoJsonComAtributoDesconhecido_quandoObjectMapperForConfigurado_iraDesserializar() throws JsonMappingException, JsonProcessingException {
      ObjectMapper mapper = new ObjectMapper();
      mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
      assertEquals("LINUS", mapper.readValue(JSON_PARA_DESSERIALIZAR, PessoaSemAnotacaoDto.class).getNome());
    }

}

Resultados:

  1. A exceção UnrecognizedPropertyException é lançada uma vez que está sendo realizada a desserialização do json utilizando a classe sem anotação e nenhuma configuração foi adicionada ao Object Mapper;
  2. A desserialização ocorre com sucesso uma vez que está sendo utilizado o DTO que possui a anotação para ignorar atributos desconhecidos;
  3. A desserialização também ocorre com sucesso pois apesar de utilizar o DTO sem a anotação para ignorar atributos desconhecidos o object mapper foi configurado com a feature FAIL_ON_UNKNOWN_PROPERTIES que ignora tais atributos.

Finalizando por hoje …

Qual a melhor abordagem? Depende.

A abordagem de anotar as classes com @JsonIgnoreProperties permite um controle mais fino sobre quais objetos devem ignorar campos desconhecidos e quais não devem. Por outro lado, um desenvolvedor desavisado pode esquecer de anotar uma classe e "provocar" a ocorrência do problema.

Já a abordagem de configurar o object mapper alinhada a um framework de injeção de dependência, garantindo que o mesmo object mapper seja utilizado em todo o sistema, garantirá a extinção da exceção em toda a aplicação, deixando no entanto os desenvolvedores de olhos vendados em relação as evoluções que ocorrerem na API sendo consumida.

E aí, curtiu? Deixa ai seu comentário com críticas, sugestões ou outras opções para resolução do problema apresentado.