Implementando um Custom Validator no Kotlin

Demis Gomes
5 min readApr 21, 2020

--

Fonte: Unsplash

Quando utilizamos Spring, a validação dos dados passados ao Controller é feita de forma direta. A biblioteca javax.validation nos dá várias opções para evitar que um objeto venha com dados inconsistentes como, por exemplo, uma data de nascimento após a data atual ou um atributo obrigatório passado como nulo.

A forma de implementar a validação é bastante conhecida para os javeiros. Porém, quando começamos a nos aventurar no Kotlin, algumas peculiaridades podem acontecer. Por isso, esse post tem como principal objetivo ajudar a quem está iniciando Kotlin ou está tentando converter (nem sempre diretamente) os códigos Java em Kotlin.

Tá, e qual seria a diferença?

Aqui, criamos um projeto no Spring Initializr simples, apenas marcando o projeto como Gradle, a Linguagem como Kotlin e a dependência Spring Web.

Esse tema noturno ficou show! :D

Abrindo o projeto na sua IDE de preferência (aqui usamos o IntelliJ), vamos criar uma classe UserController, contendo um código similar à este:

Perceba que inserimos a annotation @Valid no corpo da requisição. Desta forma, numa requisição POST, iremos validar o que o usuário mandou. Caso o User falhe em alguma validação que definimos, a requisição retornará um 400 (bad request) para o cliente.

Vamos também criar uma classe User com dois atributos: name e age. Por enquanto, nós vamos inserir uma annotation @NotBlank no name para evitar que este atributo venha vazio (“”) ou nulo.

Porém, se você perceber, caso façamos uma requisição via Postman passando um atributo name com a string “”, o user é validado com sucesso.

Cá entre nós… esse tema dark do Postman gera demais, não é?

Mas o NotBlank não evitaria isso? Vamos ver o que aconteceu!

Explicando o uso do Validator com o @field

O IntelliJ possui uma ferramenta para mostrar um código Kotlin decompilado para Java. Basta ir em Tools -> Kotlin -> Show Bytecode e depois clicar em Decompile. Nós esperávamos que o código em Java fosse muito parecido com o de Kotlin, certo?

Como debatido em um outro post da Caelum, o NotBlank vai para o parâmetro do construtor em vez do atributo. Dessa forma, o javax.validation não consegue validar o atributo na hora em que a classe é criada. Para evitar esse comportamento, precisamos entender os chamados targets da annotation, ou seja, onde ela pode ser usada.

Vamos observar o do NotBlank. Você pode acessá-lo na sua IDE (no IntelliJ é Ctrl + clique no Windows e Linux). Existe algo mais ou menos assim:

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })

Isso quer dizer que o NotBlank pode ser usado em métodos, atributos (field), tipos de annotation, construtores, parâmetros de construtor e use types. Mas por quê o bendito NotBlank foi para o parâmetro do construtor, se ele tá definido quase por último? A resposta está na documentação do Kotlin, que fala como o Kotlin gera o bytecode decompilado para Java:

If you don’t specify a use-site target, the target is chosen according to the @Target annotation of the annotation being used. If there are multiple applicable targets, the first applicable target from the following list is used:

param;

property;

field.

Então, se não definirmos o target no Kotlin quando usamos a annotation, por default, o Kotlin a insere no parâmetro do construtor caso a annotation tenha o target PARAMETER. É o caso do nosso NotBlank.

Então, é preciso usar o target @field: antes do NotBlank para que a annotation vá diretamente ao atributo da classe. Quando testarmos, veremos que a validação irá funcionar, finalmente!

É isso mesmo: Funcionar é retornar um bad request!

É válido notar que, caso decompilemos o código abaixo em Java, iremos observar o NotBlank acima do atributo name, como pretendíamos fazer desde o princípio.

Agora vamos para a melhor parte: Criar um validator customizado!

Criando um Custom Validator

Neste exemplo, imagine que estamos começando a desenvolver uma rede social que permite apenas usuários acima de 13 anos. As funções de validação padrões do javax.validation não possuem algo do tipo. Desse modo, precisamos criar uma nova validação.

Vamos criar um pacote validation e lá criar duas classes: MinimumAge e MinimumAgeValidator. A MinimumAge é a annotation propriamente dita, enquanto que a MinimumAgeValidator realiza a validação.

Aí vem outra diferença do Java para o Kotlin. Na annotation do NotBlank, por exemplo, vimos:

@Documented
@Constraint(validatedBy = { })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
public @interface NotBlank {

String message() default "{javax.validation.constraints.NotBlank.message}";

Class<?>[] groups() default { };

Class<? extends Payload>[] payload() default { };
}

Precisamos criar uma lógica parecida no nosso MinimumAge, mas veja que a “conversão” não é tão direta assim:

Os targets, retentions e o MustBeDocumented vêm de uma classe Annotations do próprio Kotlin, assim como o modificador annotation que vem antes do class. Mas nós sabemos apenas o que são os targets. E os demais?

Retention: indica quando a annotation é processada. Os mais comuns são na compilação (source) e na execução (runtime). Usamos Runtime para aproveitar os reflections que o Java permite.

MustBeDocumented: Indica que a annotation deve fazer parte da API pública na qual a classe que a anota pertence (ex: Swagger)

Constraint: classes que validam a annotation. Aí que entra o nosso MinimumAgeValidator.

Repare que o target está definido apenas como field. Você imagina o motivo de estarmos fazendo isso, correto? Vamos ver jajá…

O MinimumAgeValidator está definido assim:

É necessário implementar um ConstraintValidator com a anotação e o tipo da variável. Desse modo, sobrescrevemos o isValid() com a nossa regra de validação que, neste caso, apenas compara o valor passado à idade mínima.

Para finalizar, basta adicionar a annotation MinimumAge à nossa variável age. Como definimos apenas o target field em nossa annotation, não precisamos inserir o @field: na declaração, já que o Kotlin vai certamente inserir a annotation no atributo age quando converter em bytecode. Veja só:

A classe gerada no Decompile para Java a partir da classe acima seria:

E, fazendo a requisição via Postman passando a idade abaixo de 13 anos, vemos a mensagem de erro configurada na annotation:

Finalizando

Neste post aprendemos a criar uma validação customizada no validation usado pelo javax. Existem outras formas de validar informações em uma aplicação mas, caso haja a necessidade de unificar todas as validações em uma única implementação, a abordagem de custom validators pode ser extremamente útil.

Um ponto a se destacar é que a validação foi feita no Controller, mas a validação de idade é referente ao negócio da aplicação (é um requisito). Deste modo, a sugestão seria fazer esse tipo de validação numa camada de serviço, por exemplo. Mas isso é assunto para outros posts.

O código está disponível no Github.

E se tem algo a acrescentar, conta o que achou! Críticas, sugestões e elogios são sempre bem vindos :)

--

--

Demis Gomes

Mestre em Ciência da Computação. Atualmente é Consultor de Desenvolvimento. Gosta também de um bom futebol, Fórmula 1, Star Wars, brega e doce de leite.