Bean Validation no Kotlin

Em uma API que estávamos construindo para captar algumas avaliações de cursos, optamos por utilizar o Spring como framework juntamente com o Kotlin como linguagem de programação.

Em um dos pontos deste sistema precisávamos receber um JSON que continha algumas informações referentes a uma avaliação feita por um aluno, para depois preparar as informações que queríamos persistir.

No dia-a-dia de um desenvolvedor, é muito comum lidar com validações, com o principal objetivo de manter a consistência dos dados de uma aplicação.

Nesta aplicação não foi diferente, precisamos de algumas validações básicas para garantir consistência. Em um dos casos tínhamos um modelo que representava uma resposta:


data class Resposta(

        var observacao: String = "",

        var respostas: SortedSet<String> = sortedSetOf(),

        var idPergunta: Long = 0,

        var hashTurmaId: String = ""

)

Como algumas informações precisam ser validadas, afinal não queríamos dados inconsistentes circulando na nossa aplicação, utilizar o Bean Validation seria suficiente. Logo:


data class Resposta(

        var observacao: String = "",

        @Size(min = 1) var respostas: SortedSet<String> = sortedSetOf(),

        @NotNull var idPergunta: Long = 0,

        @NotBlank var hashTurmaId: String = ""

)

Então com a classe feita ficou faltando nosso Controller para receber a requisição. A princípio, simplesmente queremos validar se existem erros e, se existir, ele devolve um HTTP Status Code 400, senão ele devolve um Status 202:


@RequestMapping("resposta")
@RestController
class RespostaController {

    @Autowired
    private lateinit var respostaAlunoService: RespostaAlunoService

    @PostMapping(consumes = arrayOf(MediaType.APPLICATION_JSON_VALUE))
    fun salva(@Valid @RequestBody resposta: Resposta, bindingResult: BindingResult): ResponseEntity<Any> {

        if (bindingResult.hasErrors()) {

            return ResponseEntity.badRequest().build()

        }

        respostaAlunoService.save(resposta)

        return  ResponseEntity.accepted().build()

    }

}

Validando os dados

Agora que temos um mínimo necessário, chegou o momento de verificar se nossa validação usando Bean Validation trará os resultados esperados. Fazemos então uma requisição com dados consistentes:


{

    "observacao":"uma observação",

    "respostas":["uma resposta", "outra resposta"],

    "idPergunta":777,

    "hashTurmaId":"202cb962ac59075b964b07152d234b70"

}

Como esperado deu tudo certo, primeiro passo concluído.

Agora tentamos fazer uma requisição com dados inválidos, para indicar que tenho problemas de validação:


{

    "observacao":"uma observação",

    "respostas":[],

    "hashTurmaId":""

}

No final das contas ele passa de boa e não identifica que existe erros! Onde será que erramos?

Decompile para Java

Afinal, se fizermos um comparativo de como ficaria isso em Java, seria algo como:


public class Resposta {

    private String observacao;

    @Size(min = 1)
    private  SortedSet<String> respostas = new TreeSet();

    @NotNull
    private Long idPergunta;

    @NotBlank
    private String hashTurmaId;

}

No Kotlin, o bytecode gerado na compilação pode ser compatível desde Java 6 ao Java 8 , sendo assim, se pudéssemos ver como fica nosso código feito em Kotlin para Java poderíamos ver o que está acontecendo por baixo dos panos.

Na IDE IntelliJ, pertencente a empresa que desenvolveu o Kotlin, existe uma forma de decompilar o bytecode gerado a partir do Kotlin para Java. Dentro da IDE seguindo o menu Tools > Kotlin > Show Kotlin Bytecode podemos ver o bytecode gerado:

Kotlin Bytecode

No topo no lado esquerdo temos o botão Decompile e podemos assim decompilar para Java e visualizar o código equivalente:


public final class Resposta {
   @NotNull
   private String observacao;
   @NotNull
   private SortedSet<String> respostas;
   private long idPergunta;
   @NotNull
   private String hashTurmaId;

   @NotNull
   public final String getObservacao() {
      return this.observacao;
   }

   public final void setObservacao(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.observacao = var1;
   }

   @NotNull
   public final SortedSet getRespostas() {
      return this.respostas;
   }

   public final void setRespostas(@NotNull SortedSet var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.respostas = var1;
   }

   public final long getIdPergunta() {
      return this.idPergunta;
   }

   public final void setIdPergunta(long var1) {
      this.idPergunta = var1;
   }

   @NotNull
   public final String getHashTurmaId() {
      return this.hashTurmaId;
   }

   public final void setHashTurmaId(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.hashTurmaId = var1;
   }

   public Resposta(@NotNull String observacao, @Size(min = 1) @NotNull SortedSet respostas, @javax.validation.constraints.NotNull long idPergunta, @NotBlank @NotNull String hashTurmaId) {
      Intrinsics.checkParameterIsNotNull(observacao, "observacao");
      Intrinsics.checkParameterIsNotNull(respostas, "respostas");
      Intrinsics.checkParameterIsNotNull(hashTurmaId, "hashTurmaId");
      super();
      this.observacao = observacao;
      this.respostas = respostas;
      this.idPergunta = idPergunta;
      this.hashTurmaId = hashTurmaId;
   }
 }

Quando observamos as annotations que nos interessam, referente as validações, notamos que elas foram não para os atributos mas sim para o construtor. Mas, afinal, porque este comportamento?

Para responder essa pergunta temos que ver onde podemos aplicar as annotations utilizadas na validação. Veja, por exemplo, o @NotNull:


@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
    validatedBy = {}
)
public @interface NotNull {

    //trecho omitido

}

Note que no @Target fica definido a aplicabilidade da annotation, informando os locais possíveis: METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR e PARAMETER

Como fazemos em Kotlin

O que vale lembrar é que no Kotlin conseguimos acessar uma property, e não diretamente os atributos da classe então embora o Target da annotaion suporte a utilização em um field, precisamos indicar para o Kotlin onde queremos utilizar:


data class Resposta(

        var observacao: String = "",

        @field:Size(min = 1) var respostas: SortedSet<String> = sortedSetOf(),

        @field:NotNull var idPergunta: Long = 0,

        @field:NotBlank var hashTurmaId: String = ""

)

Note agora como fica o equivalente em Java após decompilar:


public final class Resposta {
   @NotNull
   private String observacao;
   @Size(
      min = 1
   )
   @NotNull
   private SortedSet<String> respostas;
   @javax.validation.constraints.NotNull
   private long idPergunta;
   @NotBlank
   @NotNull
   private String hashTurmaId;

   @NotNull
   public final String getObservacao() {
      return this.observacao;
   }

   public final void setObservacao(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.observacao = var1;
   }

   @NotNull
   public final SortedSet getRespostas() {
      return this.respostas;
   }

   public final void setRespostas(@NotNull SortedSet var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.respostas = var1;
   }

   public final long getIdPergunta() {
      return this.idPergunta;
   }

   public final void setIdPergunta(long var1) {
      this.idPergunta = var1;
   }

   @NotNull
   public final String getHashTurmaId() {
      return this.hashTurmaId;
   }

   public final void setHashTurmaId(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.hashTurmaId = var1;
   }

   public Resposta(@NotNull String observacao, @NotNull SortedSet respostas, long idPergunta, @NotNull String hashTurmaId) {
      Intrinsics.checkParameterIsNotNull(observacao, "observacao");
      Intrinsics.checkParameterIsNotNull(respostas, "respostas");
      Intrinsics.checkParameterIsNotNull(hashTurmaId, "hashTurmaId");
      super();
      this.observacao = observacao;
      this.respostas = respostas;
      this.idPergunta = idPergunta;
      this.hashTurmaId = hashTurmaId;
   }

}

Nesse caso indicamos onde exatamente queremos utilizar a annotation, e fazendo com que agora nossa validação funcione!

E você, já teve que lidar com essa situação? Usou outra forma para resolver? Comente com a gente! 😉

4 Comentários

  1. João Victor 11/12/2017 at 11:23 #

    Legal o post, utilizo bean validation nos meus sistemas java e acho uma forma fácil e prática de resolver o problema das validações. De quebra ainda conheci algo sobre kotlin, linguagem que nunca utilizei.

  2. Thiago Andrade 11/12/2017 at 13:58 #

    Legal João! Realmente o uso de bean validation ajuda bastante quando queremos fazer nossas validações.

  3. Mario Souto 19/12/2017 at 14:40 #

    Bem bacana 🙂

Deixe uma resposta