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 send
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
.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