Elevando a validação em Rails com dry-validation: Uma abordagem elegante para regras de negócio complexas
Nota importante: Este post foca especificamente na implementação e nos benefícios da gem dry-validation para validações complexas. Os controllers e serviços mostrados são intencionalmente simplificados para destacar o uso dos contratos de validação. Em um ambiente de produção, você provavelmente implementaria padrões adicionais como Service Objects, Interactors ou outras abstrações para melhorar a organização do código e a separação de responsabilidades. O Desafio do Sistema de Eventos Imagine que você está construindo um sistema de gerenciamento de eventos para uma empresa que oferece diferentes tipos de celebrações: aniversários, eventos corporativos, casamentos, etc. Cada tipo de evento tem suas próprias regras e requisitos específicos. Por exemplo, para eventos de aniversário, a empresa só aceita reservas com mais de 10 pessoas, enquanto para eventos corporativos, o mínimo é de 5 pessoas. Além disso, eventos corporativos podem ter campos específicos como 'nome da empresa'. Ao desenvolver este sistema, você rapidamente percebe que as validações tradicionais do Rails começam a tornar seu modelo Event inchado e difícil de manter. Você precisa de uma solução que: Mantenha seu modelo limpo e focado Permita regras de validação específicas para cada tipo de evento Seja fácil de estender para adicionar novos tipos de eventos no futuro No meu post anterior, exploramos como os Form Objects podem resolver esse problema. Hoje, vamos dar um passo além e conhecer a gem dry-validation, uma abordagem mais declarativa e poderosa. Por que dry-validation? A gem dry-validation faz parte do ecossistema dry-rb, que traz conceitos de programação funcional para o Ruby. Com ela, podemos criar contratos de validação com sintaxe clara e expressiva, separando completamente as regras de validação dos nossos modelos e controllers. Algumas vantagens desta abordagem: Declarativa: As regras são expressas de forma clara e concisa Composição: Fácil reutilização de validações através de herança Flexibilidade: Validações contextuais e baseadas em múltiplos atributos Mensagens de erro estruturadas: Retorno de erros em formato estruturado Integração com tipos: Possibilidade de combinar com a gem dry-types Configurando o projeto Vamos usar o mesmo projeto de exemplo do post anterior, um sistema para reserva de eventos. Primeiro, adicionamos a gem ao Gemfile: # Gemfile gem 'dry-validation', '~> 1.10' gem 'dry-types', '~> 1.5' E executamos: bundle install Agora, vamos organizar adequadamente nosso código, criando as pastas necessárias: mkdir -p app/contracts/api/v1/events mkdir -p app/types mkdir -p app/services Definindo tipos personalizados com dry-types A integração com dry-types nos permite definir tipos personalizados com validações embutidas: # frozen_string_literal: true # app/types/types.rb require "dry-types" module Types include Dry.Types() # Tipos para número de pessoas específicos para cada tipo de evento BirthdayPeople = Integer.constrained(gt: 10) BusinessPeople = Integer.constrained(gt: 5) # Tipo específico para tipos de evento válidos EventType = String.enum("birthday", "business") end Implementando contratos com dry-validation Agora, vamos criar nossos contratos de validação: Contrato Base # frozen_string_literal: true # app/contracts/api/v1/events/base_contract.rb module Api module V1 module Events class BaseContract

Nota importante: Este post foca especificamente na implementação e nos benefícios da gem dry-validation para validações complexas. Os controllers e serviços mostrados são intencionalmente simplificados para destacar o uso dos contratos de validação. Em um ambiente de produção, você provavelmente implementaria padrões adicionais como Service Objects, Interactors ou outras abstrações para melhorar a organização do código e a separação de responsabilidades.
O Desafio do Sistema de Eventos
Imagine que você está construindo um sistema de gerenciamento de eventos para uma empresa que oferece diferentes tipos de celebrações: aniversários, eventos corporativos, casamentos, etc. Cada tipo de evento tem suas próprias regras e requisitos específicos.
Por exemplo, para eventos de aniversário, a empresa só aceita reservas com mais de 10 pessoas, enquanto para eventos corporativos, o mínimo é de 5 pessoas. Além disso, eventos corporativos podem ter campos específicos como 'nome da empresa'.
Ao desenvolver este sistema, você rapidamente percebe que as validações tradicionais do Rails começam a tornar seu modelo Event
inchado e difícil de manter. Você precisa de uma solução que:
- Mantenha seu modelo limpo e focado
- Permita regras de validação específicas para cada tipo de evento
- Seja fácil de estender para adicionar novos tipos de eventos no futuro
No meu post anterior, exploramos como os Form Objects podem resolver esse problema. Hoje, vamos dar um passo além e conhecer a gem dry-validation
, uma abordagem mais declarativa e poderosa.
Por que dry-validation?
A gem dry-validation
faz parte do ecossistema dry-rb, que traz conceitos de programação funcional para o Ruby. Com ela, podemos criar contratos de validação com sintaxe clara e expressiva, separando completamente as regras de validação dos nossos modelos e controllers.
Algumas vantagens desta abordagem:
- Declarativa: As regras são expressas de forma clara e concisa
- Composição: Fácil reutilização de validações através de herança
- Flexibilidade: Validações contextuais e baseadas em múltiplos atributos
- Mensagens de erro estruturadas: Retorno de erros em formato estruturado
-
Integração com tipos: Possibilidade de combinar com a gem
dry-types
Configurando o projeto
Vamos usar o mesmo projeto de exemplo do post anterior, um sistema para reserva de eventos. Primeiro, adicionamos a gem ao Gemfile:
# Gemfile
gem 'dry-validation', '~> 1.10'
gem 'dry-types', '~> 1.5'
E executamos:
bundle install
Agora, vamos organizar adequadamente nosso código, criando as pastas necessárias:
mkdir -p app/contracts/api/v1/events
mkdir -p app/types
mkdir -p app/services
Definindo tipos personalizados com dry-types
A integração com dry-types
nos permite definir tipos personalizados com validações embutidas:
# frozen_string_literal: true
# app/types/types.rb
require "dry-types"
module Types
include Dry.Types()
# Tipos para número de pessoas específicos para cada tipo de evento
BirthdayPeople = Integer.constrained(gt: 10)
BusinessPeople = Integer.constrained(gt: 5)
# Tipo específico para tipos de evento válidos
EventType = String.enum("birthday", "business")
end
Implementando contratos com dry-validation
Agora, vamos criar nossos contratos de validação:
Contrato Base
# frozen_string_literal: true
# app/contracts/api/v1/events/base_contract.rb
module Api
module V1
module Events
class BaseContract < Dry::Validation::Contract
params do
required(:title).filled(:string)
required(:event_type).filled(::Types::EventType)
required(:number_of_people).filled(:integer)
optional(:description).maybe(:string)
optional(:special_requests).maybe(:string)
end
end
end
end
end
Contrato para Eventos de Aniversário
# frozen_string_literal: true
module Api
module V1
module Events
class BirthdayContract < BaseContract
params do
# mais campos podem ser adicionados aqui
end
rule(:number_of_people) do
result = Types::BirthdayPeople.try(value)
key.failure("must be greater than 10") if result.failure?
rescue StandardError
key.failure("is invalid")
end
end
end
end
end
Contrato para Eventos Corporativos
# frozen_string_literal: true
# app/contracts/api/v1/events/business_contract.rb
module Api
module V1
module Events
class BusinessContract < BaseContract
params do
optional(:company_name).maybe(:string)
end
rule(:number_of_people) do
result = Types::BusinessPeople.try(value)
key.failure("must be greater than 5") if result.failure?
rescue StandardError
key.failure("is invalid")
end
end
end
end
end
Criando uma fábrica de contratos
Para selecionar o contrato correto com base no tipo de evento, implementamos uma factory:
# frozen_string_literal: true
class ContractFactory
class InvalidEventTypeError < StandardError; end
INVALID_EVENT_TYPE_MSG = "Invalid event type"
def self.for(params)
case params[:event_type]
when Event.event_types[:birthday]
::Api::V1::Events::BirthdayContract.new
when Event.event_types[:business]
::Api::V1::Events::BusinessContract.new
else
raise InvalidEventTypeError, INVALID_EVENT_TYPE_MSG
end
end
end
Implementando o controller
Nosso controller utiliza a factory para selecionar o contrato adequado:
# frozen_string_literal: true
module Api
module V1
class EventsController < ApplicationController
def create
begin
contract = ::ContractFactory.for(event_params)
result = contract.call(event_params)
rescue ::ContractFactory::InvalidEventTypeError => e
return render json: { error: e.message }, status: :bad_request
end
if result.success?
create_event(result.to_h)
else
render json: { errors: result.errors.to_h }, status: :unprocessable_entity
end
end
private
def create_event(attributes)
event = Event.new(attributes)
if event.save
render json: event, status: :created
else
render json: event.errors, status: :unprocessable_entity
end
end
def event_params
params.require(:event).permit(:title, :description, :event_type, :number_of_people, :special_requests, :company_name).to_h
end
end
end
end
O modelo Event
Nosso modelo permanece simples, com apenas uma definição de enum para os tipos de evento:
# frozen_string_literal: true
class Event < ApplicationRecord
enum :event_type, { business: "business", birthday: "birthday" }
end
Testando nossa implementação
Vamos usar cURL para testar nossa API:
Evento de aniversário (sucesso)
curl -X POST \
http://localhost:3000/api/v1/events \
-H 'Content-Type: application/json' \
-d '{
"event": {
"title": "Festa de Aniversário de 30 anos",
"description": "Celebração de aniversário com amigos e família",
"event_type": "birthday",
"number_of_people": 15,
"special_requests": "Decoração em azul e branco"
}
}'
Resposta esperada (201 Created):
{
"id": 1,
"title": "Festa de Aniversário de 30 anos",
"description": "Celebração de aniversário com amigos e família",
"event_type": "birthday",
"number_of_people": 15,
"special_requests": "Decoração em azul e branco",
"created_at": "2025-03-09T12:34:56.789Z",
"updated_at": "2025-03-09T12:34:56.789Z"
}
Evento de aniversário (falha)
curl -X POST \
http://localhost:3000/api/v1/events \
-H 'Content-Type: application/json' \
-d '{
"event": {
"title": "Pequena Festa de Aniversário",
"description": "Celebração íntima de aniversário",
"event_type": "birthday",
"number_of_people": 8,
"special_requests": "Bolo de chocolate"
}
}'
Resposta esperada (422 Unprocessable Entity):
{
"errors": {
"number_of_people": ["must be greater than 10"]
}
}
Mais exemplos e cenários de teste podem ser encontrados no repositório do projeto. curls.txt
ou curl_en.txt
Principais benefícios observados
Ao utilizar a abordagem com dry-validation
e dry-types
, obtivemos várias vantagens:
- Separação de responsabilidades: Validações são tratadas fora dos modelos, mantendo-os limpos
- Código declarativo: As regras de validação são expressas de forma clara e concisa
- Reutilização através de herança: A estrutura de contratos permite herdar validações comuns
- Validações específicas por tipo: Cada tipo de evento tem suas próprias regras
- Fácil extensibilidade: Adicionar novos tipos de eventos é simples e não afeta o código existente
-
Validações baseadas em tipos: Integração com
dry-types
torna as validações ainda mais robustas
Comparação com outras abordagens
Característica | Validações no Modelo | Form Objects | dry-validation |
---|---|---|---|
Separação de responsabilidades | Baixa | Alta | Alta |
Complexidade de implementação | Baixa | Média | Média-Alta |
Clareza das regras de validação | Média | Média | Alta |
Reutilização de código | Baixa | Média | Alta |
Específico por contexto | Difícil | Possível | Fácil |
Mensagens de erro estruturadas | Não | Depende | Sim |
Integração com tipagem | Não | Não | Sim |
Conclusão
A gem dry-validation
oferece uma abordagem poderosa e flexível para lidar com validações complexas em aplicações Ruby on Rails. Especialmente em projetos com regras de negócio que variam conforme o contexto, esta técnica permite manter o código limpo, organizado e fácil de estender.
Embora exija um pouco mais de configuração inicial comparada às validações tradicionais do Rails, os benefícios se tornam evidentes à medida que o projeto cresce e as regras de validação se tornam mais complexas.
Vale ressaltar que as duas abordagens - Form Objects e dry-validation - podem coexistir e até se complementar em um projeto. A escolha depende das necessidades específicas, da complexidade das validações e das preferências da equipe.
O código completo deste exemplo está disponível no GitHub: event_reservation_system (branch: dry-validation)
Recursos adicionais
- Documentação do dry-validation
- Documentação do dry-types
- Guia de Contratos no dry-validation
- Regras com dependências no dry-validation
- Post original sobre Form Objects
Sobre o Autor
Eu sou Rodrigo Nogueira trabalho com desenvolvimento desde 2011 e com Ruby on Rails desde 2015.