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.