3

I currently have a model in which I want to use dot notation to add errors to be more specific about which key in the hash attribute is faulty:

class MyFormObject
  include ActiveModel::Validations
  include ActiveModel::AttributeAssignment

  attr_accessor :name, :metadata

  validates :name, presence: true 
  validate address_is_present_in_metadata

  def initialize(**attributes)
    assign_attributes(attributes)
  end

  def validate_address_is_present_in_metadata
    errors.add("metadata.address", "can't be blank") if metadata[:address].blank?
  end
end

This works, but if I decide to use a symbol instead of a message like the following:

errors.add("metadata.address", :blank) if metadata[:address].blank?

then Rails complains because metadata.address is not an attribute. The ActiveModel::Errors code that throws the error checks if the attribute is :base and if it's not, it tries to read the attribute from the model to generate the error message... and boom.

value = attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil)

As a workaround, I decided to override read_attribute_for_validation.

def read_attribute_for_validation(key)
  key_str = key.to_s
  return public_send(key) unless key_str.include?(".")

  key_path = key_str.split(".").map(&:to_sym)
  public_send(key_path.first).dig(*key_path[1, key_path.size])
end

Is there a better/supported way to validate nested keys in a hash that I'm not aware of?

Felix D.
  • 2,180
  • 1
  • 23
  • 37

1 Answers1

2

From the documentation :

By default this is assumed to be an instance named after the attribute. Override this method in subclasses should you need to retrieve the value for a given attribute differently.

So it seems that your strategy is valid.

Some advices though :

  • you could use public_send instead of send
  • nesting has another meaning, you could use another variable name
  • Array#[a,b] takes b elements starting at a, so b shouldn't be bigger than Array#size. You could just use array.drop(1). You might have been looking for array[a..b], which takes elements between indices a and b.
Eric Duminil
  • 52,989
  • 9
  • 71
  • 124