DEV Community

Uiratan Cavalcante
Uiratan Cavalcante

Posted on

Criando um Validador Customizado no Spring usando `Validator`

A validação de dados é um aspecto essencial no desenvolvimento de APIs para garantir a consistência e integridade das informações. O Spring oferece suporte às anotações de Bean Validation, como @NotBlank e @NotNull, mas, em alguns casos, precisamos de validações mais complexas. Um exemplo clássico é a necessidade de verificar se um valor já existe no banco de dados antes de persistir um novo registro. Neste post, vamos criar um validador customizado para evitar a duplicação de categorias utilizando a interface Validator do Spring, explicando cada etapa detalhadamente.


Passo 1: Criando a Entidade Categoria

A primeira etapa é definir a entidade Categoria, que será persistida no banco de dados. Essa entidade representa uma categoria no sistema e deve ter um nome único.

@Entity
public class Categoria {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank
    @Column(nullable = false, unique = true)
    private String nome;

    public Categoria(String nome) {
        Assert.hasText(nome, "O nome é obrigatório");
        this.nome = nome;
    }

    @Deprecated
    public Categoria() { }

    @Override
    public String toString() {
        return "Categoria{" +
                "id=" + id +
                ", nome='" + nome + '\'' +
                '}';
    }
}
Enter fullscreen mode Exit fullscreen mode

Explicação:

  • A anotação @Entity indica que esta classe será mapeada para uma tabela no banco de dados.
  • @Id e @GeneratedValue definem a chave primária e sua estratégia de geração.
  • @Column(nullable = false, unique = true) garante que o nome seja único.
  • O construtor principal recebe o nome da categoria e verifica se ele não está vazio.
  • Um construtor padrão (deprecated) é mantido para uso do JPA.

Passo 2: Criando o DTO NovaCategoriaRequest

Para evitar expor diretamente a entidade no endpoint, criamos um record chamado NovaCategoriaRequest:

public record NovaCategoriaRequest(
        @NotBlank
        String nome
) {
    public Categoria toModel() {
        return new Categoria(this.nome);
    }
}
Enter fullscreen mode Exit fullscreen mode

Explicação:

  • @NotBlank garante que o nome da categoria não pode ser vazio ou nulo.
  • O método toModel() converte o DTO para a entidade Categoria.

Passo 3: Criando o Repositório

Precisamos de um repositório para buscar categorias no banco de dados:

public interface CategoriaRepository extends CrudRepository<Categoria, Long> {
    boolean existsByNome(@NotBlank String nome);
}
Enter fullscreen mode Exit fullscreen mode

Explicação:

  • CrudRepository fornece métodos básicos para manipulação de entidades.
  • existsByNome(String nome) permite verificar se um nome já existe no banco de dados.

Passo 4: Criando o Validador Customizado

Agora criamos a classe ProibeNomeDuplicadoCategoriaValidator, que implementa a interface Validator do Spring para verificar a existência da categoria no banco de dados:

@Component
public class ProibeNomeDuplicadoCategoriaValidator implements Validator {

    private final CategoriaRepository categoriaRepository;

    public ProibeNomeDuplicadoCategoriaValidator(CategoriaRepository categoriaRepository) {
        this.categoriaRepository = categoriaRepository;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return NovaCategoriaRequest.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        if (errors.hasErrors()) {
            return;
        }

        NovaCategoriaRequest request = (NovaCategoriaRequest) target;

        if (categoriaRepository.existsByNome(request.nome())) {
            errors.rejectValue("nome", null, "Já existe uma categoria cadastrada com este nome: " + request.nome());
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Explicação:

  • Validator é uma interface do Spring usada para criar validadores customizados.
  • supports(Class<?> clazz) define para qual classe esse validador será aplicado.
  • validate(Object target, Errors errors) verifica se o nome da categoria já existe no banco e rejeita a entrada caso positivo.
  • errors.hasErrors() verifica se já há erros anteriores, evitando validações desnecessárias.
  • NovaCategoriaRequest request = (NovaCategoriaRequest) target; faz um cast seguro do objeto recebido.
  • categoriaRepository.existsByNome(request.nome()) consulta o banco para verificar se o nome já foi cadastrado.
  • errors.rejectValue("nome", null, "Já existe uma categoria cadastrada com este nome: " + request.nome()); adiciona um erro ao campo nome, impedindo a criação de categorias duplicadas.

Passo 5: Integrando o Validador no Controller

No CategoriasController, registramos o validador customizado no WebDataBinder dentro do método init:

@RestController
public class CategoriasController {

    private final EntityManager entityManager;
    private final ProibeNomeDuplicadoCategoriaValidator proibeNomeDuplicadoCategoriaValidator;

    public CategoriasController(
            final EntityManager entityManager,
            final ProibeNomeDuplicadoCategoriaValidator proibeNomeDuplicadoCategoriaValidator) {
        this.entityManager = entityManager;
        this.proibeNomeDuplicadoCategoriaValidator = proibeNomeDuplicadoCategoriaValidator;
    }

    @InitBinder
    public void init(WebDataBinder binder) {
        binder.addValidators(proibeNomeDuplicadoCategoriaValidator);
    }

    @PostMapping("/categorias")
    @ResponseStatus(HttpStatus.OK)
    @Transactional
    public String criarCategoria(@RequestBody @Valid NovaCategoriaRequest novaCategoriaRequest) {
        Categoria novaCategoria = novaCategoriaRequest.toModel();
        entityManager.persist(novaCategoria);
        return novaCategoria.toString();
    }
}
Enter fullscreen mode Exit fullscreen mode

Explicação:

  • @InitBinder garante que o validador seja aplicado antes do processamento da requisição.
  • @Valid ativa a validação antes de processar o POST.
  • entityManager.persist(novaCategoria) salva a nova categoria no banco.

O que está acontecendo aqui?

  1. Método anotado com @InitBinder

    • O Spring chama métodos marcados com @InitBinder antes de processar requisições para um controlador.
    • Esse método permite personalizar o WebDataBinder, que é responsável por converter e validar os dados da requisição antes que eles sejam passados ao método do controlador.
  2. Parâmetro WebDataBinder binder

    • O WebDataBinder gerencia a conversão e validação de objetos que chegam na requisição HTTP.
    • Ele pode ser configurado para adicionar conversores personalizados, configurações específicas e validadores.
  3. binder.addValidators(proibeNomeDuplicadoCategoriaValidator);

    • Esse trecho adiciona o validador ProibeNomeDuplicadoCategoriaValidator ao WebDataBinder.
    • Com isso, toda vez que o NovaCategoriaRequest for recebido em uma requisição, o Spring automaticamente executará esse validador antes de chamar o método do controlador.
    • Se o validador encontrar um erro (como um nome duplicado), ele adicionará esse erro ao contexto da validação, impedindo que a requisição continue.

Por que usar @InitBinder?

  • Garante que a validação seja aplicada a todas as requisições que manipulam NovaCategoriaRequest, sem precisar chamá-la explicitamente em cada endpoint.
  • Mantém o código do controlador mais limpo, pois a validação ocorre antes do método do controlador ser chamado.
  • Permite adicionar múltiplos validadores de maneira centralizada.

Em qual contexto esse método é chamado?

Sempre que um POST /categorias for feito, e o @RequestBody @Valid NovaCategoriaRequest for processado, o Spring chamará automaticamente esse @InitBinder, garantindo que o validador personalizado seja executado antes da persistência da entidade.


Passo 6: Testando o Validador

Requisição Válida:

{
    "nome": "Tecnologia"
}
Enter fullscreen mode Exit fullscreen mode

Resposta:

HTTP 200 OK
{
    "id": 1,
    "nome": "Tecnologia"
}
Enter fullscreen mode Exit fullscreen mode

Requisição Duplicada:

{
    "nome": "Tecnologia"
}
Enter fullscreen mode Exit fullscreen mode

Resposta:

HTTP 400 BAD REQUEST
{
    "errors": [
        {
            "field": "nome",
            "message": "Já existe uma categoria cadastrada com este nome: Tecnologia"
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

Conclusão

Criar validadores customizados no Spring com Validator permite adicionar regras de negócio específicas sem poluir o controlador. Com essa abordagem, garantimos que a lógica de validação fique separada da lógica de controle, tornando o código mais modular e fácil de manter.

Se você deseja um sistema robusto e confiável, considere usar validadores customizados para garantir a integridade dos seus dados! 🚀


Speedy emails, satisfied customers

Postmark Image

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs