Validaciones de Active Record

Esta guía te enseña cómo validar el estado de los objetos antes de que ingresen a la base de datos utilizando la funcionalidad de validaciones de Active Record.

Después de leer esta guía, sabrás:


1 Visión General de las Validaciones

Aquí hay un ejemplo de una validación muy simple:

class Person < ApplicationRecord
  validates :name, presence: true
end
irb> Person.create(name: "John Doe").valid?
=> true
irb> Person.create(name: nil).valid?
=> false

Como puedes ver, nuestra validación nos informa que nuestro Person no es válido sin un atributo name. El segundo Person no se guardará en la base de datos.

Antes de profundizar en más detalles, hablemos de cómo las validaciones encajan en el panorama general de tu aplicación.

1.1 ¿Por qué Usar Validaciones?

Las validaciones se utilizan para asegurar que solo datos válidos se guarden en tu base de datos. Por ejemplo, puede ser importante para tu aplicación asegurarse de que cada usuario proporcione una dirección de correo electrónico y una dirección postal válidas. Las validaciones a nivel de modelo son la mejor manera de asegurar que solo datos válidos se guarden en tu base de datos. Son independientes de la base de datos, no pueden ser eludidas por los usuarios finales, y son convenientes para probar y mantener. Rails proporciona ayudantes integrados para necesidades comunes y te permite crear tus propios métodos de validación también.

Hay varias otras maneras de validar datos antes de que se guarden en tu base de datos, incluyendo restricciones nativas de la base de datos, validaciones del lado del cliente y validaciones a nivel de controlador. Aquí hay un resumen de los pros y contras:

  • Las restricciones de la base de datos y/o los procedimientos almacenados hacen que los mecanismos de validación dependan de la base de datos y pueden hacer que las pruebas y el mantenimiento sean más difíciles. Sin embargo, si tu base de datos es utilizada por otras aplicaciones, puede ser una buena idea usar algunas restricciones a nivel de base de datos. Además, las validaciones a nivel de base de datos pueden manejar de manera segura algunas cosas (como la unicidad en tablas muy utilizadas) que pueden ser difíciles de implementar de otra manera.
  • Las validaciones del lado del cliente pueden ser útiles, pero generalmente son poco confiables si se usan solas. Si se implementan usando JavaScript, pueden ser eludidas si JavaScript está desactivado en el navegador del usuario. Sin embargo, si se combinan con otras técnicas, la validación del lado del cliente puede ser una manera conveniente de proporcionar retroalimentación inmediata a los usuarios mientras usan tu sitio.
  • Las validaciones a nivel de controlador pueden ser tentadoras de usar, pero a menudo se vuelven inmanejables y difíciles de probar y mantener. Siempre que sea posible, es una buena idea mantener tus controladores simples, ya que hará que tu aplicación sea un placer de trabajar a largo plazo.

Elige estas opciones en ciertos casos específicos. Es la opinión del equipo de Rails que las validaciones a nivel de modelo son las más apropiadas en la mayoría de las circunstancias.

1.2 ¿Cuándo Ocurre la Validación?

Hay dos tipos de objetos Active Record: aquellos que corresponden a una fila dentro de tu base de datos y aquellos que no. Cuando creas un objeto nuevo, por ejemplo usando el método new, ese objeto aún no pertenece a la base de datos. Una vez que llamas a save sobre ese objeto, se guardará en la tabla de base de datos apropiada. Active Record utiliza el método de instancia new_record? para determinar si un objeto ya está en la base de datos o no. Considera la siguiente clase de Active Record:

class Person < ApplicationRecord
end

Podemos ver cómo funciona observando algunos resultados de bin/rails console:

irb> p = Person.new(name: "John Doe")
=> #<Person id: nil, name: "John Doe", created_at: nil, updated_at: nil>

irb> p.new_record?
=> true

irb> p.save
=> true

irb> p.new_record?
=> false

Crear y guardar un nuevo registro enviará una operación SQL INSERT a la base de datos. Actualizar un registro existente enviará una operación SQL UPDATE en su lugar. Las validaciones generalmente se ejecutan antes de que estos comandos se envíen a la base de datos. Si alguna validación falla, el objeto será marcado como no válido y Active Record no realizará la operación INSERT o UPDATE. Esto evita almacenar un objeto no válido en la base de datos. Puedes elegir que se ejecuten validaciones específicas cuando un objeto se crea, guarda o actualiza.

PRECAUCIÓN: Hay muchas maneras de cambiar el estado de un objeto en la base de datos. Algunos métodos activarán validaciones, pero otros no. Esto significa que es posible guardar un objeto en la base de datos en un estado no válido si no tienes cuidado.

Los siguientes métodos activan validaciones y guardarán el objeto en la base de datos solo si el objeto es válido:

Las versiones con signo de exclamación (por ejemplo, save!) lanzan una excepción si el registro es inválido. Las versiones sin signo de exclamación no lo hacen: save y update devuelven false, y create devuelve el objeto.

1.3 Omitiendo Validaciones

Los siguientes métodos omiten validaciones y guardarán el objeto en la base de datos independientemente de su validez. Deben usarse con precaución. Consulta la documentación del método para obtener más información.

Ten en cuenta que save también tiene la capacidad de omitir validaciones si se pasa validate: false como argumento. Esta técnica debe usarse con precaución.

  • save(validate: false)

1.4 valid? e invalid?

Antes de guardar un objeto Active Record, Rails ejecuta tus validaciones. Si estas validaciones producen algún error, Rails no guarda el objeto.

También puedes ejecutar estas validaciones por tu cuenta. valid? activa tus validaciones y devuelve true si no se encontraron errores en el objeto, y false en caso contrario. Como viste arriba:

class Person < ApplicationRecord
  validates :name, presence: true
end
irb> Person.create(name: "John Doe").valid?
=> true
irb> Person.create(name: nil).valid?
=> false

Después de que Active Record ha realizado las validaciones, cualquier falla puede ser accedida a través del método de instancia errors, que devuelve una colección de errores. Por definición, un objeto es válido si esta colección está vacía después de ejecutar las validaciones.

Ten en cuenta que un objeto instanciado con new no reportará errores incluso si es técnicamente inválido, porque las validaciones se ejecutan automáticamente solo cuando el objeto se guarda, como con los métodos create o save.

class Person < ApplicationRecord
  validates :name, presence: true
end
irb> p = Person.new
=> #<Person id: nil, name: nil>
irb> p.errors.size
=> 0

irb> p.valid?
=> false
irb> p.errors.objects.first.full_message
=> "Name can't be blank"

irb> p = Person.create
=> #<Person id: nil, name: nil>
irb> p.errors.objects.first.full_message
=> "Name can't be blank"

irb> p.save
=> false

irb> p.save!
ActiveRecord::RecordInvalid: Validation failed: Name can't be blank

irb> Person.create!
ActiveRecord::RecordInvalid: Validation failed: Name can't be blank

invalid? es la inversa de valid?. Activa tus validaciones, devolviendo true si se encontraron errores en el objeto, y false en caso contrario.

1.5 errors[]

Para verificar si un atributo particular de un objeto es válido, puedes usar errors[:attribute]. Devuelve un array de todos los mensajes de error para :attribute. Si no hay errores en el atributo especificado, se devuelve un array vacío.

Este método solo es útil después de que las validaciones se hayan ejecutado, porque solo inspecciona la colección de errores y no activa las validaciones por sí mismo. Es diferente del método ActiveRecord::Base#invalid? explicado anteriormente porque no verifica la validez del objeto en su conjunto. Solo comprueba si hay errores encontrados en un atributo individual del objeto.

class Person < ApplicationRecord
  validates :name, presence: true
end
irb> Person.new.errors[:name].any?
=> false
irb> Person.create.errors[:name].any?
=> true

Cubriremos los errores de validación en mayor profundidad en la sección Trabajando con Errores de Validación.

2 Ayudantes de Validación

Active Record ofrece muchos ayudantes de validación predefinidos que puedes usar directamente dentro de tus definiciones de clase. Estos ayudantes proporcionan reglas de validación comunes. Cada vez que una validación falla, se agrega un error a la colección errors del objeto, y esto se asocia con el atributo que se está validando.

Cada ayudante acepta un número arbitrario de nombres de atributos, por lo que con una sola línea de código puedes agregar el mismo tipo de validación a varios atributos.

Todos ellos aceptan las opciones :on y :message, que definen cuándo se debe ejecutar la validación y qué mensaje se debe agregar a la colección errors si falla, respectivamente. La opción :on toma uno de los valores :create o :update. Hay un mensaje de error predeterminado para cada uno de los ayudantes de validación. Estos mensajes se utilizan cuando no se especifica la opción :message. Echemos un vistazo a cada uno de los ayudantes disponibles.

Para ver una lista de los ayudantes predeterminados disponibles, echa un vistazo a ActiveModel::Validations::HelperMethods.

2.1 acceptance

Este método valida que un checkbox en la interfaz de usuario fue marcado cuando se envió un formulario. Esto se utiliza típicamente cuando el usuario necesita aceptar los términos de servicio de tu aplicación, confirmar que se ha leído un texto o cualquier concepto similar.

class Person < ApplicationRecord
  validates :terms_of_service, acceptance: true
end

Esta verificación se realiza solo si terms_of_service no es nil. El mensaje de error predeterminado para este ayudante es "must be accepted". También puedes pasar un mensaje personalizado a través de la opción message.

class Person < ApplicationRecord
  validates :terms_of_service, acceptance: { message: 'must be abided' }
end

También puede recibir una opción :accept, que determina los valores permitidos que se considerarán como aceptables. Por defecto es ['1', true] y se puede cambiar fácilmente.

class Person < ApplicationRecord
  validates :terms_of_service, acceptance: { accept: 'yes' }
  validates :eula, acceptance: { accept: ['TRUE', 'accepted'] }
end

Esta validación es muy específica para aplicaciones web y esta 'aceptación' no necesita registrarse en ningún lugar de tu base de datos. Si no tienes un campo para ello, el ayudante creará un atributo virtual. Si el campo existe en tu base de datos, la opción accept debe establecerse en o incluir true o de lo contrario la validación no se ejecutará.

2.2 confirmation

Debes usar este ayudante cuando tengas dos campos de texto que deben recibir exactamente el mismo contenido. Por ejemplo, es posible que desees confirmar una dirección de correo electrónico o una contraseña. Esta validación crea un atributo virtual cuyo nombre es el nombre del campo que debe confirmarse con "_confirmation" añadido.

class Person < ApplicationRecord
  validates :email, confirmation: true
end

En tu plantilla de vista podrías usar algo como

<%= text_field :person, :email %>
<%= text_field :person, :email_confirmation %>

NOTA: Esta verificación se realiza solo si email_confirmation no es nil. Para requerir confirmación, asegúrate de agregar una verificación de presencia para el atributo de confirmación (veremos presence más adelante en esta guía):

class Person < ApplicationRecord
  validates :email, confirmation: true
  validates :email_confirmation, presence: true
end

También hay una opción :case_sensitive que puedes usar para definir si la restricción de confirmación será sensible a mayúsculas o no. Esta opción por defecto es true.

class Person < ApplicationRecord
  validates :email, confirmation: { case_sensitive: false }
end

El mensaje de error predeterminado para este ayudante es "doesn't match confirmation". También puedes pasar un mensaje personalizado a través de la opción message.

Generalmente, cuando usas este validador, querrás combinarlo con la opción :if para validar el campo "_confirmation" solo cuando el campo inicial haya cambiado y no cada vez que guardes el registro. Más sobre validaciones condicionales más adelante.

class Person < ApplicationRecord
  validates :email, confirmation: true
  validates :email_confirmation, presence: true, if: :email_changed?
end

2.3 comparison

Esta verificación validará una comparación entre dos valores comparables.

class Promotion < ApplicationRecord
  validates :end_date, comparison: { greater_than: :start_date }
end

El mensaje de error predeterminado para este ayudante es "failed comparison". También puedes pasar un mensaje personalizado a través de la opción message.

Estas opciones son todas compatibles:

  • :greater_than - Especifica que el valor debe ser mayor que el valor suministrado. El mensaje de error predeterminado para esta opción es "must be greater than %{count}".
  • :greater_than_or_equal_to - Especifica que el valor debe ser mayor o igual al valor suministrado. El mensaje de error predeterminado para esta opción es "must be greater than or equal to %{count}".
  • :equal_to - Especifica que el valor debe ser igual al valor suministrado. El mensaje de error predeterminado para esta opción es "must be equal to %{count}".
  • :less_than - Especifica que el valor debe ser menor que el valor suministrado. El mensaje de error predeterminado para esta opción es "must be less than %{count}".
  • :less_than_or_equal_to - Especifica que el valor debe ser menor o igual al valor suministrado. El mensaje de error predeterminado para esta opción es "must be less than or equal to %{count}".
  • :other_than - Especifica que el valor debe ser diferente al valor suministrado. El mensaje de error predeterminado para esta opción es "must be other than %{count}".

NOTA: El validador requiere que se proporcione una opción de comparación. Cada opción acepta un valor, proc o símbolo. Cualquier clase que incluya Comparable puede ser comparada.

2.4 format

Este ayudante valida los valores de los atributos probando si coinciden con una expresión regular dada, que se especifica usando la opción :with.

class Product < ApplicationRecord
  validates :legacy_code, format: { with: /\A[a-zA-Z]+\z/,
    message: "only allows letters" }
end

Inversamente, al usar la opción :without en su lugar, puedes requerir que el atributo especificado no coincida con la expresión regular.

En cualquier caso, la opción :with o :without proporcionada debe ser una expresión regular o un proc o lambda que devuelva una.

El mensaje de error predeterminado es "is invalid".

ADVERTENCIA. usa \A y \z para coincidir con el inicio y el final de la cadena, ^ y $ coinciden con el inicio/final de una línea. Debido al uso frecuente incorrecto de ^ y $, necesitas pasar la opción multiline: true en caso de que uses alguno de estos dos anclajes en la expresión regular proporcionada. En la mayoría de los casos, deberías estar usando \A y \z.

2.5 inclusion

Este ayudante valida que los valores de los atributos estén incluidos en un conjunto dado. De hecho, este conjunto puede ser cualquier objeto enumerable.

class Coffee < ApplicationRecord
  validates :size, inclusion: { in: %w(small medium large),
    message: "%{value} is not a valid size" }
end

El ayudante inclusion tiene una opción :in que recibe el conjunto de valores que serán aceptados. La opción :in tiene un alias llamado :within que puedes usar para el mismo propósito, si lo deseas. El ejemplo anterior usa la opción :message para mostrar cómo puedes incluir el valor del atributo. Para ver todas las opciones, consulta la documentación del mensaje.

El mensaje de error predeterminado para este ayudante es "is not included in the list".

2.6 exclusion

El opuesto de inclusion es... ¡exclusion!

Este ayudante valida que los valores de los atributos no estén incluidos en un conjunto dado. De hecho, este conjunto puede ser cualquier objeto enumerable.

class Account < ApplicationRecord
  validates :subdomain, exclusion: { in: %w(www us ca jp),
    message: "%{value} is reserved." }
end

El ayudante exclusion tiene una opción :in que recibe el conjunto de valores que no serán aceptados para los atributos validados. La opción :in tiene un alias llamado :within que puedes usar para el mismo propósito, si lo deseas. Este ejemplo usa la opción :message para mostrar cómo puedes incluir el valor del atributo. Para ver todas las opciones del argumento del mensaje, consulta la documentación del mensaje.

El mensaje de error predeterminado es "is reserved".

Alternativamente a un enumerable tradicional (como un Array), puedes proporcionar un proc, lambda o símbolo que devuelva un enumerable. Si el enumerable es un rango numérico, de tiempo o de fecha y hora, la prueba se realiza con Range#cover?, de lo contrario con include?. Cuando se usa un proc o lambda, se pasa la instancia bajo validación como argumento.

2.7 length

Este ayudante valida la longitud de los valores de los atributos. Proporciona una variedad de opciones, por lo que puedes especificar restricciones de longitud de diferentes maneras:

class Person < ApplicationRecord
  validates :name, length: { minimum: 2 }
  validates :bio, length: { maximum: 500 }
  validates :password, length: { in: 6..20 }
  validates :registration_number, length: { is: 6 }
end

Las posibles opciones de restricción de longitud son:

  • :minimum - El atributo no puede tener menos de la longitud especificada.
  • :maximum - El atributo no puede tener más de la longitud especificada.
  • :in (o :within) - La longitud del atributo debe estar incluida en un intervalo dado. El valor para esta opción debe ser un rango.
  • :is - La longitud del atributo debe ser igual al valor dado.

Los mensajes de error predeterminados dependen del tipo de validación de longitud que se esté realizando. Puedes personalizar estos mensajes usando las opciones :wrong_length, :too_long y :too_short, y %{count} como un marcador de posición para el número correspondiente a la restricción de longitud que se está utilizando. Aún puedes usar la opción :message para especificar un mensaje de error.

class Person < ApplicationRecord
  validates :bio, length: { maximum: 1000,
    too_long: "%{count} characters is the maximum allowed" }
end

Ten en cuenta que los mensajes de error predeterminados son plurales (por ejemplo, "is too short (minimum is %{count} characters)"). Por esta razón, cuando :minimum es 1, deberías proporcionar un mensaje personalizado o usar presence: true en su lugar. Cuando :in o :within tienen un límite inferior de 1, deberías proporcionar un mensaje personalizado o llamar a presence antes de length.

NOTA: Solo se puede usar una opción de restricción a la vez, aparte de las opciones :minimum y :maximum, que se pueden combinar juntas.

2.8 numericality

Este ayudante valida que tus atributos tengan solo valores numéricos. Por defecto, coincidirá con un signo opcional seguido de un número entero o decimal.

Para especificar que solo se permiten números enteros, establece :only_integer en true. Luego usará la siguiente expresión regular para validar el valor del atributo.

/\A[+-]?\d+\z/

De lo contrario, intentará convertir el valor a un número usando Float. Los Float se convierten a BigDecimal usando el valor de precisión de la columna o un máximo de 15 dígitos.

class Player < ApplicationRecord
  validates :points, numericality: true
  validates :games_played, numericality: { only_integer: true }
end

El mensaje de error predeterminado para :only_integer es "must be an integer".

Además de :only_integer, este ayudante también acepta la opción :only_numeric que especifica que el valor debe ser una instancia de Numeric e intenta analizar el valor si es una String.

NOTA: Por defecto, numericality no permite valores nil. Puedes usar la opción allow_nil: true para permitirlo. Ten en cuenta que para las columnas Integer y Float, las cadenas vacías se convierten en nil.

El mensaje de error predeterminado cuando no se especifican opciones es "is not a number".

También hay muchas opciones que se pueden usar para agregar restricciones a los valores aceptables:

  • :greater_than - Especifica que el valor debe ser mayor que el valor suministrado. El mensaje de error predeterminado para esta opción es "must be greater than %{count}".
  • :greater_than_or_equal_to - Especifica que el valor debe ser mayor o igual al valor suministrado. El mensaje de error predeterminado para esta opción es "must be greater than or equal to %{count}".
  • :equal_to - Especifica que el valor debe ser igual al valor suministrado. El mensaje de error predeterminado para esta opción es "must be equal to %{count}".
  • :less_than - Especifica que el valor debe ser menor que el valor suministrado. El mensaje de error predeterminado para esta opción es "must be less than %{count}".
  • :less_than_or_equal_to - Especifica que el valor debe ser menor o igual al valor suministrado. El mensaje de error predeterminado para esta opción es "must be less than or equal to %{count}".
  • :other_than - Especifica que el valor debe ser diferente al valor suministrado. El mensaje de error predeterminado para esta opción es "must be other than %{count}".
  • :in - Especifica que el valor debe estar en el rango suministrado. El mensaje de error predeterminado para esta opción es "must be in %{count}".
  • :odd - Especifica que el valor debe ser un número impar. El mensaje de error predeterminado para esta opción es "must be odd".
  • :even - Especifica que el valor debe ser un número par. El mensaje de error predeterminado para esta opción es "must be even".

2.9 presence

Este ayudante valida que los atributos especificados no estén vacíos. Utiliza el método Object#blank? para comprobar si el valor es nil o una cadena vacía, es decir, una cadena que esté vacía o que consista solo en espacios en blanco.

class Person < ApplicationRecord
  validates :name, :login, :email, presence: true
end

Si deseas asegurarte de que una asociación esté presente, necesitarás verificar si el objeto asociado en sí está presente, y no solo la clave foránea utilizada para mapear la asociación. De esta manera, no solo se verifica que la clave foránea no esté vacía, sino también que el objeto referenciado exista.

class Supplier < ApplicationRecord
  has_one :account
  validates :account, presence: true
end

Para validar registros asociados cuya presencia es requerida, debes especificar la opción :inverse_of para la asociación:

class Order < ApplicationRecord
  has_many :line_items, inverse_of: :order
end

NOTA: Si deseas asegurarte de que la asociación esté tanto presente como válida, también necesitas usar validates_associated. Más sobre eso abajo.

Si validas la presencia de un objeto asociado a través de una relación has_one o has_many, verificará que el objeto no sea blank? ni marked_for_destruction?.

Dado que false.blank? es true, si deseas validar la presencia de un campo booleano, debes usar una de las siguientes validaciones:

# El valor _debe_ ser true o false
validates :boolean_field_name, inclusion: [true, false]
# El valor _no debe_ ser nil, es decir, true o false
validates :boolean_field_name, exclusion: [nil]

Al usar una de estas validaciones, te asegurarás de que el valor NO sea nil, lo que resultaría en un valor NULL en la mayoría de los casos.

El mensaje de error predeterminado es "can't be blank".

2.10 absence

Este ayudante valida que los atributos especificados estén ausentes. Utiliza el método Object#present? para comprobar si el valor no es nil o una cadena vacía, es decir, una cadena que esté vacía o que consista solo en espacios en blanco.

class Person < ApplicationRecord
  validates :name, :login, :email, absence: true
end

Si deseas asegurarte de que una asociación esté ausente, necesitarás verificar si el objeto asociado en sí está ausente, y no solo la clave foránea utilizada para mapear la asociación.

class LineItem < ApplicationRecord
  belongs_to :order
  validates :order, absence: true
end

Para validar registros asociados cuya ausencia es requerida, debes especificar la opción :inverse_of para la asociación:

class Order < ApplicationRecord
  has_many :line_items, inverse_of: :order
end

NOTA: Si deseas asegurarte de que la asociación esté tanto presente como válida, también necesitas usar validates_associated. Más sobre eso abajo.

Si validas la ausencia de un objeto asociado a través de una relación has_one o has_many, verificará que el objeto no sea present? ni marked_for_destruction?.

Dado que false.present? es false, si deseas validar la ausencia de un campo booleano, debes usar validates :field_name, exclusion: { in: [true, false] }.

El mensaje de error predeterminado es "must be blank".

2.11 uniqueness

Este ayudante valida que el valor del atributo sea único justo antes de que el objeto se guarde.

class Account < ApplicationRecord
  validates :email, uniqueness: true
end

La validación ocurre realizando una consulta SQL en la tabla del modelo, buscando un registro existente con el mismo valor en ese atributo.

Hay una opción :scope que puedes usar para especificar uno o más atributos que se utilizan para limitar la verificación de unicidad:

class Holiday < ApplicationRecord
  validates :name, uniqueness: { scope: :year,
    message: "should happen once per year" }
end

ADVERTENCIA. Esta validación no crea una restricción de unicidad en la base de datos, por lo que puede suceder que dos conexiones diferentes a la base de datos creen dos registros con el mismo valor para una columna que deseas que sea única. Para evitar eso, debes crear un índice único en esa columna en tu base de datos.

Para agregar una restricción de unicidad en tu base de datos, usa la declaración add_index en una migración e incluye la opción unique: true.

Si deseas crear una restricción de base de datos para evitar posibles violaciones de una validación de unicidad usando la opción :scope, debes crear un índice único en ambas columnas en tu base de datos. Consulta el manual de MySQL y el manual de MariaDB para obtener más detalles sobre índices de columnas múltiples, o el manual de PostgreSQL para ejemplos de restricciones únicas que se refieren a un grupo de columnas.

También hay una opción :case_sensitive que puedes usar para definir si la restricción de unicidad será sensible a mayúsculas, insensible a mayúsculas o si debe respetar la colación predeterminada de la base de datos. Esta opción por defecto es respetar la colación predeterminada de la base de datos.

class Person < ApplicationRecord
  validates :name, uniqueness: { case_sensitive: false }
end

ADVERTENCIA. Ten en cuenta que algunas bases de datos están configuradas para realizar búsquedas insensibles a mayúsculas de todos modos.

Hay una opción :conditions que puedes especificar como un fragmento SQL WHERE para limitar la búsqueda de la restricción de unicidad (por ejemplo, conditions: -> { where(status: 'active') }).

El mensaje de error predeterminado es "has already been taken".

Consulta validates_uniqueness_of para obtener más información.

2.12 validates_associated

Debes usar este ayudante cuando tu modelo tenga asociaciones que siempre necesiten ser validadas. Cada vez que intentes guardar tu objeto, se llamará a valid? en cada uno de los objetos asociados.

class Library < ApplicationRecord
  has_many :books
  validates_associated :books
end

Esta validación funcionará con todos los tipos de asociación.

PRECAUCIÓN: No uses validates_associated en ambos extremos de tus asociaciones. Se llamarían entre sí en un bucle infinito.

El mensaje de error predeterminado para validates_associated es "is invalid". Ten en cuenta que cada objeto asociado contendrá su propia colección errors; los errores no se propagan al modelo que llama.

NOTA: validates_associated solo se puede usar con objetos ActiveRecord, hasta ahora todo lo mencionado también se puede usar en cualquier objeto que incluya ActiveModel::Validations.

2.13 validates_each

Este ayudante valida atributos contra un bloque. No tiene una función de validación predefinida. Debes crear una usando un bloque, y cada atributo pasado a validates_each será probado contra él.

En el siguiente ejemplo, rechazaremos nombres y apellidos que comiencen con minúsculas.

class Person < ApplicationRecord
  validates_each :name, :surname do |record, attr, value|
    record.errors.add(attr, 'must start with upper case') if /\A[[:lower:]]/.match?(value)
  end
end

El bloque recibe el registro, el nombre del atributo y el valor del atributo.

Puedes hacer cualquier cosa que desees para verificar datos válidos dentro del bloque. Si tu validación falla, debes agregar un error al modelo, haciéndolo inválido.

2.14 validates_with

Este ayudante pasa el registro a una clase separada para la validación.

class GoodnessValidator < ActiveModel::Validator
  def validate(record)
    if record.first_name == "Evil"
      record.errors.add :base, "This person is evil"
    end
  end
end

class Person < ApplicationRecord
  validates_with GoodnessValidator
end

No hay mensaje de error predeterminado para validates_with. Debes agregar manualmente errores a la colección errors del registro en la clase del validador.

NOTA: Los errores agregados a record.errors[:base] se relacionan con el estado del registro en su conjunto.

Para implementar el método de validación, debes aceptar un parámetro record en la definición del método, que es el registro a validar.

Si deseas agregar un error en un atributo específico, pásalo como primer argumento, como record.errors.add(:first_name, "please choose another name"). Cubriremos errores de validación en mayor detalle más adelante.

def validate(record)
  if record.some_field != "acceptable"
    record.errors.add :some_field, "this field is unacceptable"
  end
end

El ayudante validates_with toma una clase, o una lista de clases para usar en la validación.

class Person < ApplicationRecord
  validates_with MyValidator, MyOtherValidator, on: :create
end

Como todas las demás validaciones, validates_with toma las opciones :if, :unless y :on. Si pasas cualquier otra opción, las enviará a la clase del validador como options:

class GoodnessValidator < ActiveModel::Validator
  def validate(record)
    if options[:fields].any? { |field| record.send(field) == "Evil" }
      record.errors.add :base, "This person is evil"
    end
  end
end

class Person < ApplicationRecord
  validates_with GoodnessValidator, fields: [:first_name, :last_name]
end

Ten en cuenta que el validador se inicializará solo una vez para todo el ciclo de vida de la aplicación, y no en cada ejecución de validación, así que ten cuidado al usar variables de instancia dentro de él.

Si tu validador es lo suficientemente complejo como para que desees variables de instancia, puedes usar fácilmente un objeto Ruby normal en su lugar:

class Person < ApplicationRecord
  validate do |person|
    GoodnessValidator.new(person).validate
  end
end

class GoodnessValidator
  def initialize(person)
    @person = person
  end

  def validate
    if some_complex_condition_involving_ivars_and_private_methods?
      @person.errors.add :base, "This person is evil"
    end
  end

  # ...
end

Cubriremos validaciones personalizadas más adelante.

3 Opciones Comunes de Validación

Hay varias opciones comunes compatibles con los validadores que acabamos de revisar, ¡vamos a repasarlas ahora!

NOTA: No todas estas opciones son compatibles con cada validador, consulta la documentación de la API para ActiveModel::Validations.

Al usar cualquiera de los métodos de validación que acabamos de mencionar, también hay una lista de opciones comunes compartidas junto con los validadores. ¡Las cubriremos ahora!

  • :allow_nil: Omite la validación si el atributo es nil.
  • :allow_blank: Omite la validación si el atributo está en blanco.
  • :message: Especifica un mensaje de error personalizado.
  • :on: Especifica los contextos donde esta validación está activa.
  • :strict: Lanza una excepción cuando la validación falla.
  • :if y :unless: Especifica cuándo la validación debe o no debe ocurrir.

3.1 :allow_nil

La opción :allow_nil omite la validación cuando el valor que se está validando es nil.

class Coffee < ApplicationRecord
  validates :size, inclusion: { in: %w(small medium large),
    message: "%{value} is not a valid size" }, allow_nil: true
end
irb> Coffee.create(size: nil).valid?
=> true
irb> Coffee.create(size: "mega").valid?
=> false

Para ver todas las opciones del argumento del mensaje, consulta la documentación del mensaje.

3.2 :allow_blank

La opción :allow_blank es similar a la opción :allow_nil. Esta opción permitirá que la validación pase si el valor del atributo es blank?, como nil o una cadena vacía, por ejemplo.

class Topic < ApplicationRecord
  validates :title, length: { is: 5 }, allow_blank: true
end
irb> Topic.create(title: "").valid?
=> true
irb> Topic.create(title: nil).valid?
=> true

3.3 :message

Como ya has visto, la opción :message te permite especificar el mensaje que se agregará a la colección errors cuando la validación falle. Cuando no se usa esta opción, Active Record usará el mensaje de error predeterminado respectivo para cada ayudante de validación.

La opción :message acepta un String o Proc como su valor.

Un valor de String para :message puede contener opcionalmente cualquiera/todos de %{value}, %{attribute} y %{model}, que serán reemplazados dinámicamente cuando la validación falle. Este reemplazo se realiza usando la gema i18n, y los marcadores de posición deben coincidir exactamente, no se permiten espacios.

class Person < ApplicationRecord
  # Mensaje codificado
  validates :name, presence: { message: "must be given please" }

  # Mensaje con valor de atributo dinámico. %{value} será reemplazado
  # con el valor real del atributo. %{attribute} y %{model}
  # también están disponibles.
  validates :age, numericality: { message: "%{value} seems wrong" }
end

Un valor de Proc para :message recibe dos argumentos: el objeto que se está validando y un hash con pares clave-valor :model, :attribute y :value.

class Person < ApplicationRecord
  validates :username,
    uniqueness: {
      # objeto = objeto persona que se está validando
      # datos = { model: "Person", attribute: "Username", value: <username> }
      message: ->(object, data) do
        "Hey #{object.name}, #{data[:value]} is already taken."
      end
    }
end

3.4 :on

La opción :on te permite especificar cuándo debe suceder la validación. El comportamiento predeterminado para todos los ayudantes de validación integrados es ejecutarse al guardar (tanto cuando estás creando un nuevo registro como cuando lo estás actualizando). Si deseas cambiarlo, puedes usar on: :create para ejecutar la validación solo cuando se crea un nuevo registro o on: :update para ejecutar la validación solo cuando se actualiza un registro.

class Person < ApplicationRecord
  # será posible actualizar el correo electrónico con un valor duplicado
  validates :email, uniqueness: true, on: :create

  # será posible crear el registro con una edad no numérica
  validates :age, numericality: true, on: :update

  # el valor predeterminado (valida tanto en la creación como en la actualización)
  validates :name, presence: true
end

También puedes usar on: para definir contextos personalizados. Los contextos personalizados deben activarse explícitamente pasando el nombre del contexto a valid?, invalid? o save.

class Person < ApplicationRecord
  validates :email, uniqueness: true, on: :account_setup
  validates :age, numericality: true, on: :account_setup
end
irb> person = Person.new(age: 'treinta y tres')
irb> person.valid?
=> true
irb> person.valid?(:account_setup)
=> false
irb> person.errors.messages
=> {:email=>["has already been taken"], :age=>["is not a number"]}

person.valid?(:account_setup) ejecuta ambas validaciones sin guardar el modelo. person.save(context: :account_setup) valida person en el contexto account_setup antes de guardar.

Pasar un array de símbolos también es aceptable.

class Book
  include ActiveModel::Validations

  validates :title, presence: true, on: [:update, :ensure_title]
end
irb> book = Book.new(title: nil)
irb> book.valid?
=> true
irb> book.valid?(:ensure_title)
=> false
irb> book.errors.messages
=> {:title=>["can't be blank"]}

Cuando se activa mediante un contexto explícito, las validaciones se ejecutan para ese contexto, así como para cualquier validación sin un contexto.

class Person < ApplicationRecord
  validates :email, uniqueness: true, on: :account_setup
  validates :age, numericality: true, on: :account_setup
  validates :name, presence: true
end
irb> person = Person.new
irb> person.valid?(:account_setup)
=> false
irb> person.errors.messages
=> {:email=>["has already been taken"], :age=>["is not a number"], :name=>["can't be blank"]}

Cubriremos más casos de uso para on: en la guía de callbacks.

4 Validaciones Estrictas

También puedes especificar validaciones para que sean estrictas y lanzar ActiveModel::StrictValidationFailed cuando el objeto sea inválido.

class Person < ApplicationRecord
  validates :name, presence: { strict: true }
end
irb> Person.new.valid?
ActiveModel::StrictValidationFailed: Name can't be blank

También existe la posibilidad de pasar una excepción personalizada a la opción :strict.

class Person < ApplicationRecord
  validates :token, presence: true, uniqueness: true, strict: TokenGenerationException
end
irb> Person.new.valid?
TokenGenerationException: Token can't be blank

5 Validación Condicional

A veces tendrá sentido validar un objeto solo cuando se cumpla un predicado dado. Puedes hacer eso usando las opciones :if y :unless, que pueden tomar un símbolo, un Proc o un Array. Puedes usar la opción :if cuando quieras especificar cuándo la validación debe ocurrir. Alternativamente, si deseas especificar cuándo la validación no debe ocurrir, entonces puedes usar la opción :unless.

5.1 Usando un Símbolo con :if y :unless

Puedes asociar las opciones :if y :unless con un símbolo correspondiente al nombre de un método que se llamará justo antes de que ocurra la validación. Esta es la opción más comúnmente utilizada.

class Order < ApplicationRecord
  validates :card_number, presence: true, if: :paid_with_card?

  def paid_with_card?
    payment_type == "card"
  end
end

5.2 Usando un Proc con :if y :unless

Es posible asociar :if y :unless con un objeto Proc que se llamará. Usar un objeto Proc te da la capacidad de escribir una condición en línea en lugar de un método separado. Esta opción es más adecuada para condiciones de una sola línea.

class Account < ApplicationRecord
  validates :password, confirmation: true,
    unless: Proc.new { |a| a.password.blank? }
end

Como lambda es un tipo de Proc, también se puede usar para escribir condiciones en línea aprovechando la sintaxis abreviada.

validates :password, confirmation: true, unless: -> { password.blank? }

5.3 Agrupando Validaciones Condicionales

A veces es útil que múltiples validaciones usen una condición. Esto se puede lograr fácilmente usando with_options.

class User < ApplicationRecord
  with_options if: :is_admin? do |admin|
    admin.validates :password, length: { minimum: 10 }
    admin.validates :email, presence: true
  end
end

Todas las validaciones dentro del bloque with_options habrán pasado automáticamente la condición if: :is_admin?.

5.4 Combinando Condiciones de Validación

Por otro lado, cuando múltiples condiciones definen si una validación debe ocurrir o no, se puede usar un Array. Además, puedes aplicar tanto :if como :unless a la misma validación.

class Computer < ApplicationRecord
  validates :mouse, presence: true,
                    if: [Proc.new { |c| c.market.retail? }, :desktop?],
                    unless: Proc.new { |c| c.trackpad.present? }
end

La validación solo se ejecuta cuando todas las condiciones :if y ninguna de las condiciones :unless se evalúan como true.

6 Realizando Validaciones Personalizadas

Cuando los ayudantes de validación integrados no son suficientes para tus necesidades, puedes escribir tus propios validadores o métodos de validación según lo prefieras.

6.1 Validadores Personalizados

Los validadores personalizados son clases que heredan de ActiveModel::Validator. Estas clases deben implementar el método validate, que toma un registro como argumento y realiza la validación sobre él. El validador personalizado se llama usando el método validates_with.

class MyValidator < ActiveModel::Validator
  def validate(record)
    unless record.name.start_with? 'X'
      record.errors.add :name, "Provide a name starting with X, please!"
    end
  end
end

class Person < ApplicationRecord
  validates_with MyValidator
end

La forma más sencilla de agregar validadores personalizados para validar atributos individuales es con el conveniente ActiveModel::EachValidator. En este caso, la clase de validador personalizado debe implementar un método validate_each que toma tres argumentos: registro, atributo y valor. Estos corresponden a la instancia, el atributo a validar y el valor del atributo en la instancia pasada.

class EmailValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    unless URI::MailTo::EMAIL_REGEXP.match?(value)
      record.errors.add attribute, (options[:message] || "is not an email")
    end
  end
end

class Person < ApplicationRecord
  validates :email, presence: true, email: true
end

Como se muestra en el ejemplo, también puedes combinar validaciones estándar con tus propios validadores personalizados.

6.2 Métodos Personalizados

También puedes crear métodos que verifiquen el estado de tus modelos y agreguen errores a la colección errors cuando sean inválidos. Debes registrar estos métodos usando el método de clase validate, pasando los símbolos para los nombres de los métodos de validación.

Puedes pasar más de un símbolo para cada método de clase y las respectivas validaciones se ejecutarán en el mismo orden en que fueron registradas.

El método valid? verificará que la colección errors esté vacía, por lo que tus métodos de validación personalizados deben agregar errores a ella cuando desees que la validación falle:

class Invoice < ApplicationRecord
  validate :expiration_date_cannot_be_in_the_past,
    :discount_cannot_be_greater_than_total_value

  def expiration_date_cannot_be_in_the_past
    if expiration_date.present? && expiration_date < Date.today
      errors.add(:expiration_date, "can't be in the past")
    end
  end

  def discount_cannot_be_greater_than_total_value
    if discount > total_value
      errors.add(:discount, "can't be greater than total value")
    end
  end
end

Por defecto, tales validaciones se ejecutarán cada vez que llames a valid? o guardes el objeto. Pero también es posible controlar cuándo ejecutar estas validaciones personalizadas dando una opción :on al método validate, con cualquiera de estos valores: :create o :update.

class Invoice < ApplicationRecord
  validate :active_customer, on: :create

  def active_customer
    errors.add(:customer_id, "is not active") unless customer.active?
  end
end

Consulta la sección anterior para obtener más detalles sobre :on.

6.3 Listando Validadores

Si deseas conocer todos los validadores para un objeto dado, no busques más allá de validators.

Por ejemplo, si tenemos el siguiente modelo usando un validador personalizado y un validador integrado:

class Person < ApplicationRecord
  validates :name, presence: true, on: :create
  validates :email, format: URI::MailTo::EMAIL_REGEXP
  validates_with MyOtherValidator, strict: true
end

Ahora podemos usar validators en el modelo "Person" para listar todos los validadores, o incluso verificar un campo específico usando validators_on.

irb> Person.validators
#=> [#<ActiveRecord::Validations::PresenceValidator:0x10b2f2158
      @attributes=[:name], @options={:on=>:create}>,
     #<MyOtherValidatorValidator:0x10b2f17d0
      @attributes=[:name], @options={:strict=>true}>,
     #<ActiveModel::Validations::FormatValidator:0x10b2f0f10
      @attributes=[:email],
      @options={:with=>URI::MailTo::EMAIL_REGEXP}>]
     #<MyOtherValidator:0x10b2f0948 @options={:strict=>true}>]

irb> Person.validators_on(:name)
#=> [#<ActiveModel::Validations::PresenceValidator:0x10b2f2158
      @attributes=[:name], @options={on: :create}>]

7 Trabajando con Errores de Validación

Los métodos valid? e invalid? solo proporcionan un estado resumido sobre la validez. Sin embargo, puedes profundizar en cada error individual usando varios métodos de la colección errors.

La siguiente es una lista de los métodos más comúnmente usados. Consulta la documentación de ActiveModel::Errors para obtener una lista de todos los métodos disponibles.

7.1 errors

La puerta de entrada a través de la cual puedes profundizar en varios detalles de cada error.

Esto devuelve una instancia de la clase ActiveModel::Errors que contiene todos los errores, cada error está representado por un objeto ActiveModel::Error.

class Person < ApplicationRecord
  validates :name, presence: true, length: { minimum: 3 }
end
irb> person = Person.new
irb> person.valid?
=> false
irb> person.errors.full_messages
=> ["Name can't be blank", "Name is too short (minimum is 3 characters)"]

irb> person = Person.new(name: "John Doe")
irb> person.valid?
=> true
irb> person.errors.full_messages
=> []

irb> person = Person.new
irb> person.valid?
=> false
irb> person.errors.first.details
=> {:error=>:too_short, :count=>3}

7.2 errors[]

errors[] se usa cuando deseas verificar los mensajes de error para un atributo específico. Devuelve un array de cadenas con todos los mensajes de error para el atributo dado, cada cadena con un mensaje de error. Si no hay errores relacionados con el atributo, devuelve un array vacío.

class Person < ApplicationRecord
  validates :name, presence: true, length: { minimum: 3 }
end
irb> person = Person.new(name: "John Doe")
irb> person.valid?
=> true
irb> person.errors[:name]
=> []

irb> person = Person.new(name: "JD")
irb> person.valid?
=> false
irb> person.errors[:name]
=> ["is too short (minimum is 3 characters)"]

irb> person = Person.new
irb> person.valid?
=> false
irb> person.errors[:name]
=> ["can't be blank", "is too short (minimum is 3 characters)"]

7.3 errors.where y Objeto de Error

A veces podemos necesitar más información sobre cada error además de su mensaje. Cada error está encapsulado como un objeto ActiveModel::Error, y el método where es la forma más común de acceso.

where devuelve un array de objetos de error filtrados por varios grados de condiciones.

class Person < ApplicationRecord
  validates :name, presence: true, length: { minimum: 3 }
end

Podemos filtrar solo por el attribute pasándolo como primer parámetro a errors.where(:attr). El segundo parámetro se usa para filtrar el type de error que queremos llamando a errors.where(:attr, :type).

irb> person = Person.new
irb> person.valid?
=> false

irb> person.errors.where(:name)
=> [ ... ] # todos los errores para el atributo :name

irb> person.errors.where(:name, :too_short)
=> [ ... ] # errores :too_short para el atributo :name

Por último, podemos filtrar por cualquier options que pueda existir en el tipo de error dado.

irb> person = Person.new
irb> person.valid?
=> false

irb> person.errors.where(:name, :too_short, minimum: 3)
=> [ ... ] # todos los errores de nombre siendo demasiado cortos y el mínimo es 2

Puedes leer varias informaciones de estos objetos de error:

irb> error = person.errors.where(:name).last

irb> error.attribute
=> :name
irb> error.type
=> :too_short
irb> error.options[:count]
=> 3

También puedes generar el mensaje de error:

irb> error.message
=> "is too short (minimum is 3 characters)"
irb> error.full_message
=> "Name is too short (minimum is 3 characters)"

El método full_message genera un mensaje más amigable para el usuario, con el nombre del atributo capitalizado precedido. (Para personalizar el formato que usa full_message, consulta la guía de I18n.)

7.4 errors.add

El método add crea el objeto de error tomando el attribute, el type de error y un hash de opciones adicionales. Esto es útil al escribir tu propio validador, ya que te permite definir situaciones de error muy específicas.

class Person < ApplicationRecord
  validate do |person|
    errors.add :name, :too_plain, message: "is not cool enough"
  end
end
irb> person = Person.create
irb> person.errors.where(:name).first.type
=> :too_plain
irb> person.errors.where(:name).first.full_message
=> "Name is not cool enough"

7.5 errors[:base]

Puedes agregar errores que estén relacionados con el estado del objeto en su conjunto, en lugar de estar relacionados con un atributo específico. Para hacer esto, debes usar :base como el atributo al agregar un nuevo error.

class Person < ApplicationRecord
  validate do |person|
    errors.add :base, :invalid, message: "This person is invalid because ..."
  end
end
irb> person = Person.create
irb> person.errors.where(:base).first.full_message
=> "This person is invalid because ..."

7.6 errors.size

El método size devuelve el número total de errores para el objeto.

class Person < ApplicationRecord
  validates :name, presence: true, length: { minimum: 3 }
end
irb> person = Person.new
irb> person.valid?
=> false
irb> person.errors.size
=> 2

irb> person = Person.new(name: "Andrea", email: "andrea@example.com")
irb> person.valid?
=> true
irb> person.errors.size
=> 0

7.7 errors.clear

El método clear se usa cuando intencionalmente deseas borrar la colección errors. Por supuesto, llamar a errors.clear sobre un objeto inválido no lo hará realmente válido: la colección errors ahora estará vacía, pero la próxima vez que llames a valid? o cualquier método que intente guardar este objeto en la base de datos, las validaciones se ejecutarán nuevamente. Si alguna de las validaciones falla, la colección errors se llenará nuevamente.

class Person < ApplicationRecord
  validates :name, presence: true, length: { minimum: 3 }
end
irb> person = Person.new
irb> person.valid?
=> false
irb> person.errors.empty?
=> false

irb> person.errors.clear
irb> person.errors.empty?
=> true

irb> person.save
=> false

irb> person.errors.empty?
=> false

8 Mostrando Errores de Validación en Vistas

Una vez que has creado un modelo y agregado validaciones, si ese modelo se crea a través de un formulario web, probablemente quieras mostrar un mensaje de error cuando una de las validaciones falle.

Debido a que cada aplicación maneja este tipo de cosas de manera diferente, Rails no incluye ningún ayudante de vista para ayudarte a generar estos mensajes directamente. Sin embargo, debido a la rica cantidad de métodos que Rails te ofrece para interactuar con las validaciones en general, puedes construir los tuyos propios. Además, al generar un scaffold, Rails colocará algo de ERB en el _form.html.erb que genera, que muestra la lista completa de errores en ese modelo.

Suponiendo que tenemos un modelo que se ha guardado en una variable de instancia llamada @article, se ve así:

<% if @article.errors.any? %>
  <div id="error_explanation">
    <h2><%= pluralize(@article.errors.count, "error") %> prohibited this article from being saved:</h2>

    <ul>
      <% @article.errors.each do |error| %>
        <li><%= error.full_message %></li>
      <% end %>
    </ul>
  </div>
<% end %>

Además, si usas los ayudantes de formulario de Rails para generar tus formularios, cuando ocurre un error de validación en un campo, generará un <div> adicional alrededor de la entrada.

<div class="field_with_errors">
  <input id="article_title" name="article[title]" size="30" type="text" value="">
</div>

Luego puedes estilizar este div como desees. El scaffold predeterminado que genera Rails, por ejemplo, agrega esta regla CSS:

.field_with_errors {
  padding: 2px;
  background-color: red;
  display: table;
}

Esto significa que cualquier campo con un error termina con un borde rojo de 2 píxeles.


Comentarios

Se te anima a ayudar a mejorar la calidad de esta guía.

Por favor contribuye si ves algún error tipográfico o errores fácticos. Para comenzar, puedes leer nuestra sección de contribuciones a la documentación.

También puedes encontrar contenido incompleto o cosas que no están actualizadas. Por favor agrega cualquier documentación faltante para main. Asegúrate de revisar Guías Edge primero para verificar si los problemas ya están resueltos o no en la rama principal. Revisa las Guías de Ruby on Rails para estilo y convenciones.

Si por alguna razón detectas algo que corregir pero no puedes hacerlo tú mismo, por favor abre un issue.

Y por último, pero no menos importante, cualquier tipo de discusión sobre la documentación de Ruby on Rails es muy bienvenida en el Foro oficial de Ruby on Rails.