Introdução
Esses dias surgiu uma situação a ser resolvida no trabalho e resolvi compartilhar a solução aqui no dev.to!
Vou tentar resumir o problema o máximo que eu conseguir:
Tínhamos uma API em Java SpringBoot que persistia informação em um banco de dados PostgreSQL.
Essa API recebia um objeto no request que era composto por cerca de 200 campos, a maioria deles eram strings.
Um tempo após o desenvolvimento ter sido concluído, surgiu a necessidade de todas as informações textuais serem persistidas em upper case (TIPO ASSIM, COM CAPS LOCK).
O time, então, se reuniu para decidir qual abordagem seguiríamos para modificar a api em questão.
A primeira alternativa proposta era receber todos os campos e, manualmente, utilizar o método toUpperCase() da classe String para converter atributo por atributo para upper case.
Como a API tinha certa complexidade, essa solução poderia acabar levando muito tempo e comprometendo a entrega da squad.
Como trabalhamos com prazos apertados, pensei em uma outra abordagem para diminuir o custo de tempo da solução: fazer um deserializador personalizado para que, todos os campos string sejam convertidos em upper case na chegada do request, ao ser mapeado para um objeto na entrada do endpoint da API.
Esse desenvolvimento levou cerca de 30 minutos para ser concluído, entre pesquisa e desenvolvimento, economizando um bom tempo de trabalho na equipe e funcionou muito bem.
Vou compartilhar com vocês o que foi desenvolvido utilizando uma pequena API de exemplo de caso de uso. Gostaria de deixar claro que o objetivo aqui não é se esta é a melhor prática (isso você decide com seu time de acordo com suas necessidades e disponibilidades), também não me preocupei em fazer o código mais a prova de falhas aqui na demonstração, então não coloquei, por exemplo, try catch pois este não é o foco deste artigo.
Para quem quiser ver o **repositório **do código utilizado, aqui está o link:
Repositório GitHub
No repositório você irá encontrar a branch main com o código base do projeto antes da implementação.
Encontrará, também, a branch onde foi implementado o deserializador customizado e, também, uma branch onde implementei um serializador customizado caso você queira que a aplicação seja na saída, na hora de montar o JSON que sai da sua aplicação.
(Vou falar sobre o serializador no próximo artigo!)
Apresentando a aplicação:
Para exemplificarmos, criei uma pequena aplicação que possui como porta de entrada o endpoint POST /students, que espera receber no corpo da requisição um JSON que será mapeado para um objeto da classe StudentRequest. Segue abaixo o código da classe StudentController.java
@RestController
public class StudentController {
@Autowired
private StudentService studentService;
@PostMapping("/students")
ResponseEntity<StudentResponse> createStudent(@RequestBody StudentRequest studentRequest) {
return ResponseEntity.ok().body(studentService.createStudent(studentRequest));
}
}
Podemos ver que esta classe está recebendo injeção do service que irá tratar das regras de negócio.
A nossa classe de Request está desta maneira (StudentRequest.java):
@Data
public class StudentRequest {
private Long registration;
private String name;
private String lastName;
}
O response tem os mesmos campos, porém com nome de StudentResponse.
Este service, o StudentService.java terá as regras de negócio para depois persistir as informações do Student no banco de dados. Não me importei em implementar a persistência no banco pois não é o foco aqui, mas basicamente teríamos um service desta forma:
@Service
public class StudentServiceImpl implements StudentService {
@Autowired
private StudentMapper studentMapper;
@Override
public StudentResponse createStudent(StudentRequest studentRequest) {
//some business logics
return studentMapper.requestToResponse(studentRequest);
}
}
Perceba que aqui, para encurtar transformações da classe de request para a classe de entidade ou response estou utilizando o MapStruct (escreverei sobre o MapStruct em outro artigo).
Ao rodar o programa e chamar o endpoint, temos o seguinte resultado:
Certo, o código escrito desta forma (que vocês podem ver na branch main do repositório) acaba persistindo as informações como elas chegam no request, sem nenhum tratamento ou conversão dos campos em upper case como queremos. Vamos, então, à implementação do nosso Deserializador, para que os campos String passem pelo toUpperCase() logo na entrada da requisição.
Implementando o Deserializador:
Essa solução é incrivelmente simples, vamos apenas criar uma classe chamada ToUpperCaseConverter.java e faremos ela extender a classe StdConverter do Jackson.
Dentro do operador diamante ( <> ) colocaremos dois tipos de objeto, um de entrada (IN) e outro de saída (OUT), ou seja, o StdConverter tem como "assinatura", StdConverter. Como vamos receber uma String e devolver uma String porém em upper case, vamos utilizar StdConverter.
E então vamos sobrescrever o método convert(String value) colocando nosso toUpperCase() na implementação deste método. Ficando assim nossa classe:
import com.fasterxml.jackson.databind.util.StdConverter;
public class ToUpperCaseConverter extends StdConverter<String, String> {
@Override
public String convert(String value) {
return value.toUpperCase();
}
}
Essa classe, para fins de organização, coloquei dentro de um pacote chamado utils.
Agora, como fazemos para marcar os campos que precisam passar por essa conversão? Para isso basta irmos na nossa classe de request e adicionar a anotação @JsonDeserialize e passar para ela qual classe que irá aplicar a deserialização customizada que criamos, ou seja, a classe ToUpperCaseConverter.java. Vamos adicionar essa anotação em todos os campos que precisamos fazer essa conversão, ficando, assim, nossa classe de Request:
@Data
public class StudentRequest {
private Long registration;
@JsonDeserialize(converter = ToUpperCaseConverter.class)
private String name;
@JsonDeserialize(converter = ToUpperCaseConverter.class)
private String lastName;
}
Agora, podemos colocar nossa API para rodar e vermos o resultado, como mostrado no print do POSTMAN abaixo:
Você pode fazer muitas adaptações para esta lógica inclusive criando um Deserializador customizado para toda a classe de Request! Passando campo a campo instruindo que manipulação você quer fazer com o campo, mas isso fica para outra postagem.
Espero que tenha conseguido contribuir e dúvidas e sugestões podem ser enviadas aqui, fico muito grato caso tenham melhorias a sugerir.
Top comments (7)
Achei legal a solução. Obrigado por compartilhar.
Eu fiquei com uma dúvida. Por que usar
toUpperCase()
demoraria quase 1 semana?Não seria apenas uma questão de adicionar os setters? Por exemplo:
Entendo que escrever 200 destes seja uma tarefa tediosa, mas vc poderia usar a IDE para criar os métodos. Ajeitar estes 200 métodos gerados pela IDE continua sendo tedioso, mas estimar em 1 semana não foi demais?
A solução com o deserializer é bem útil, mas não dificulta os testes de unidade? Ter este comportamente direto nos setter faria o teste mais fácil de configurar eu acho.
Um outro ponto aqui é que esse artigo tem uma intenção didática de compartilhamento de conhecimento. Esse exemplo utilizado realmente é bem simples e banal. A ideia é que isso seja o primeiro passo para que quem leia entenda o funcionamento para desenvolver seu deserializador mais complexo por conta própria. Que isso seja só o pontapé inicial :)
Opa, excelente contribuição e muito obrigado. Existem diferentes formas de fazer essa solução. A sua super funciona também e é ótima inclusive! Uma outra forma poderia ser na hora de mapear uma entidade utilizando mapstruct por exemplo. A estimativa do TL realmente foi altíssima. Mas é um padrão aqui onde trabalho, as estimativas são mais longas do que a realidade. As etapas são longas, uma tarefa fica um certo tempo no desenvolvimento, depois fica mais tempo no desenvolvimento de testes e depois mais tempo ainda na parte dos testes com o QA, acredito que por isso ele pediu 1 semana.
Sobre os testes: o que resolveu aqui foi realizar testes integrados, assim o comportamento foi validado nos testes, inclusive tá vindo uma postagem sobre testes no futuro.
Entendi, há todo um processo até por em produção. Obrigado por responder.
Agora me surgiu mais uma dúvida: Vc já sabia que levaria 30 minutos desde o início ou vc estimou um tempo maior, tipo 1 ou 2 dias, e só depois percebeu que seria ainda mais rápido e terminou antes do prazo?
Eu sabia da possibilidade de criar um deserializador personalizado. Quando o TL chutou essa estimativa de 1 semana (pelo que entendi dele foi um chute pra cima mesmo), eu falei na reunião que tentaria uma outra abordagem pra acelerar isso ai sem ter que ficar fazendo "sets". Inclusive, a minha solução foi diferente dessa. A solução que utilizei foi um serializador, pois fiz a modificação numa aplicação producer, que envia para o kafka (você pode ver isso na segunda publicação). Mas eu não estimei nada pra eles, aqui onde estou trabalhando atualmente nós não trabalhamos com "sprints" e acabamos não fazendo muitas estimativas. Apenas mapeamos o que precisa ser feito no quarter, e criamos as histórias com as tarefas e vamos fazendo, sem estimar muito. Nessa situação específica falamos em estimativas pois é uma modificação em algo que já estava pronto.
Ah e complementando a resposta pra sua pergunta: eu não tinha a mínima ideia de quanto tempo levaria, eu sabia que a implementação seria rápida, mas eu nunca tinha feito um deserializador assim, então levei mais tempo pesquisando como fazer do que fazendo mesmo haha
Legal. Parece que vcs tem bastante liberdade então, o processo não é engessado.
Acho muito bom posts como este porque eu mesmo sou sozinho no projeto que trabalho e ainda não tenho experiencias trabalhando em times com liders e tudo.
Obrigado.
Certo, qualquer coisa que precisar, é só chamar!