Armadilha em Desserializadores Jackson: Nulos Silenciosos por Incompatibilidade de Tipo

Ao desenvolver desserializadores customizados em Jackson para tipos como String, é comum estender JsonDeserializer<String> e sobrescrever o método deserialize. No entanto, a falta de validação do tipo do token JSON atual pode levar a uma falha sutil: incompatibilidades de tipo são silenciosamente ignoradas, resultando em valores null em vez de exceções esperadas.

O Problema no Código

Considere um desserializador customizado para String que, de forma ingênua, tenta obter o valor como uma string sem verificar o tipo de token atual. Este é um exemplo de código com essa vulnerabilidade:


import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;

import java.io.IOException;

public class DesserializadorTextoVulneravel extends JsonDeserializer<String> {

    @Override
    public String deserialize(JsonParser parser, DeserializationContext contexto) throws IOException {
        // Acessa o valor como string sem verificar o tipo de token.
        // Se o token não for uma string (ex: array, objeto, número),
        // getValueAsString() retornará null, ocultando o erro.
        return parser.getValueAsString();
    }
}

Demonstração Prática

Para ilustrar o comportamento, usaremos uma classe de entidade simples e um programa principal que testa a desserialização com e sem o desserializador customizado.


// Entidade de Exemplo
class DadosUsuario {
    private String nome;

    public String getNome() { return nome; }
    public void setNome(String nome) { this.nome = nome; }

    @Override
    public String toString() {
        return "DadosUsuario{nome='" + nome + "'}";
    }
}

O programa a seguir demonstra a diferença de comportamento ao tentar desserializar um campo String a partir de um JSON que contém um array:


import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;

public class TesteDesserializacaoTexto {

    public static void main(String[] args) throws Exception {
        System.out.println("--- Teste sem Desserializador Customizado ---");
        executarTestePadrao();
        System.out.println("\n--- Teste com Desserializador Customizado Vulnerável ---");
        executarTesteVulneravel();
    }

    private static void executarTestePadrao() {
        ObjectMapper mapeador = new ObjectMapper();
        String jsonEntrada = "{\"nome\": [1,2]}"; // Array onde uma string é esperada

        System.out.println("JSON de Entrada: " + jsonEntrada);
        try {
            DadosUsuario usuario = mapeador.readValue(jsonEntrada, DadosUsuario.class);
            System.out.println("Resultado da Desserialização: " + usuario.getNome());
        } catch (Exception e) {
            System.out.println("Erro Capturado: " + e.getClass().getSimpleName());
        }
    }

    private static void executarTesteVulneravel() {
        ObjectMapper mapeador = new ObjectMapper();
        SimpleModule modulo = new SimpleModule("ModuloTexto");
        modulo.addDeserializer(String.class, new DesserializadorTextoVulneravel());
        mapeador.registerModule(modulo);

        String jsonEntrada = "{\"nome\": [1,2]}"; // Array onde uma string é esperada

        System.out.println("JSON de Entrada: " + jsonEntrada);
        try {
            DadosUsuario usuario = mapeador.readValue(jsonEntrada, DadosUsuario.class);
            System.out.println("Resultado da Desserialização: " + usuario.getNome());
        } catch (Exception e) {
            System.out.println("Erro Capturado: " + e.getClass().getSimpleName());
        }
    }
}

Resultados da Execução

Ao executar o código acima, observamos os seguintes resultados:


--- Teste sem Desserializador Customizado ---
JSON de Entrada: {"nome": [1,2]}
Erro Capturado: MismatchedInputException

--- Teste com Desserializador Customizado Vulnerável ---
JSON de Entrada: {"nome": [1,2]}
Resultado da Desserialização: null

Como pode ser visto, o comportamento padrão do Jackson lança um MismatchedInputException, indicando claramente que um array não pode ser desserializado para um String. No entanto, com o DesserializadorTextoVulneravel, a exceção é engolida e o campo nome torna-se silenciosamente null.

A Causa Raiz e a Solução

O método JsonParser.getValueAsString(), quando chamado em um token que não representa um valor de string (como START_ARRAY, START_OBJECT ou VALUE_NUMBER), retorna null em vez de lançar uma exceção. Este comportamento, embora útil em certos contextos, torna-se uma armadilha em desserializadores customizados de String, pois esconde erros de tipagem.

A solução correta envolve verificar o tipo de token atual (parser.currentToken()) antes de tentar extrair o valor. Se o token não for o esperado (JsonToken.VALUE_STRING), uma exceção apropriada como MismatchedInputException deve ser lançada. Isso garante que o comportamento do desserializador customizado seja consistente com o padrão do Jackson, fornecendo feedback claro sobre erros de entrada.


// Desserializador Correto com Validação de Token
public class DesserializadorTextoSeguro extends JsonDeserializer<String> {

    @Override
    public String deserialize(JsonParser parser, DeserializationContext contexto) throws IOException {
        if (parser.currentToken() != JsonToken.VALUE_STRING) {
            // Lança uma exceção se o token não for do tipo string esperado
            throw MismatchedInputException.from(
                parser,
                String.class,
                "Esperado um valor de string, mas encontrado: " + parser.currentToken()
            );
        }
        return parser.getValueAsString();
    }
}

Ao substituir DesserializadorTextoVulneravel por DesserializadorTextoSeguro no método executarTesteVulneravel() e reexecutar, o resultado será idêntico ao executarTestePadrao(), lançando MismatchedInputException e corrigindo a falha.

Tags: Jackson java Desserialização JsonParser MismatchedInputException

Publicado em 7-16 21:50