Dry-validation (v1.0.0rc3) - dynamic keys

I’m moving validation from ActiveRecord to dry-validation and I need to validate nested hash with dynamic keys:

{ 
  ...
  "users_attributes" => { 
    "0" => { "name" => "John Doe" }, 
    "1" => { "name" => "Dan Brown" },
    ...
  } 
}

I don’t care about keys (eg “0”, “1”), but I’d like to validate each value (eg required(:name).filled(:string))

Is there any way to do so with dry-schema (>= v1.0.2) and/or dry-validation (>= v1.0.0.rc3)?

This is currently not supported by the schema. I’ll add support for dynamic keys in 1.x but for now you could work around this limitation by using contract rules:

class MyContract < Dry::Validation::Contract
  UserAttribute = Dry::Schema.Params do
    required(:name).filled(:string)
  end

  params do
    required(:users_attributes).filled(:hash)
  end

  rule(:users_attributes) do
    values[:users_attributes].each do |key, value|
      res = UserAttribute.(value)

      next if res.success?

      key([:users_attributes, key]).failure(res.to_h)
    end
  end
end

Thanks, unfortunately key([:users_attributes, key]).failure(res.to_h) doesn’t work.
The only solution I found:

res.errors.to_h.each do |name, messages|
  messages.each do |msg|
    key([key_name, name]).failure(msg)
  end
end

The downside of rule(:user_attributes) that it won’t remove unknown keys

attrs = {
  "users_attributes" => {
    "0" => { "wrong_key" => 0, "name" => "John Doe" },
  }
}

MyContract.new.(attrs).to_h
=> {:users_attributes=>{"0"=>{"unknown_key"=>0, "name"=>"John Doe"}}}

Is there any way to remove unknown_key from the result, so I can get rid of strong_parameters?

Ah right, key processing won’t be triggered at all in such a case. I’m afraid this really has to wait until we have proper support for dynamic keys :frowning:

Got it,

It seems the best solution, for now, is to convert hashes into array

class MyContract < Dry::Validation::Contract
  params do
    required(:users_attributes).array(:hash) do
      required(:name).filled(:string)
    end
  end
end

attrs = {
  "users_attributes" => {
    "0" => { "unknown_key" => 0, "name" => "John Doe" },
    "1" => { "a" => "Dan Brown" },
  }.values
}

result = MyContract.new.(attrs)
result.to_h # => {:users_attributes=>[{:name=>"John Doe"}, {}]}
result.errors.to_h # => {:users_attributes=>{1=>{:name=>["is missing"]}}}
1 Like