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?
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 :
public_send instead of sendArray#[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.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With