dry-validation: Rule fired for optional attribute which is missing

Description

Rule is getting fired for a required attribute which is nested within an optional attribute and when the whole optional hash is missing (which should be allowed) it reports an error causing the validation to fail.

To Reproduce

Create a contract with a rule for an optional hash attribute with a required key + a rule for the required key and run with an input not including that hash. I use custom macro and I don’t know if it’s related to the bug.

So given:

class SignUpContract < Dry::Validation::Contract
  params do
    required(:user).hash do
      optional(:id) { filled? & str? }
      optional(:first_name) { filled? & str? }
      optional(:last_name) { filled? & str? }
      required(:email) { filled? & str? }
      required(:password) { filled? & str? }
      required(:accepts_terms) { filled? & bool? }
      optional(:freelancer).hash do
        required(:id) { filled? & str? }
      end
      optional(:clients).array(:hash) do
        required(:id) { filled? & str? }
      end
    end
  end

  rule('user.id').validate(:uuid_format)
  rule('user.email').validate(:email_format)
  rule('user.password').validate(min_length: 8)

  rule(user: %i[freelancer clients]) do
    freelancer = values.dig(:user, :freelancer)
    clients = values.dig(:user, :clients)

    unless freelancer ^ (clients && clients.size == 1)
      key(:user).failure(:profile_missing)
    end
  end

  rule('user.freelancer.id').validate(:uuid_format)
  rule('user.clients') do
    have_valid_uuid = lambda { |client|
      client[:id].nil? || client[:id].match?(Macros::UUID_REGEXP)
    }

    unless value.all?(have_valid_uuid)
      key.failure('not a valid UUID format')
    end
  end
end

And params:

{
  "user": {
    "id": "d16dfef3-50f9-4588-b7d7-837801e403cd",
    "first_name": "Blah",
    "last_name": "Blah",
    "email": "blah@blah.blah",
    "password": "blahblahblah",
    "accepts_terms": true,
    "clients": [
      {
        "id": "d287fdff-78ac-45ed-871b-b1b5c9548399"
      }
    ]
  }
}

It produces:

#<Dry::Validation::Result{:user=>{:id=>"d16dfef3-50f9-4588-b7d7-837801e403cd", :first_name=>"Blah", :last_name=>"Blah", :email=>"blah@blah.blah", :password=>"blahblahblah", :accepts_terms=>true, :clients=>[{:id=>"d287fdff-78ac-45ed-871b-b1b5c9548399"}]}} errors={:user=>{:freelancer=>{:id=>["not a valid UUID format"]}}}>

Expected behavior

The validation rule for optional attribute should never be run when it’s missing. In the example rules related to freelancer attributes should not be run and the validation should pass

My environment

  • Affects my production application: NO as I haven’t upgraded yet
  • Ruby version: 2.6.3
  • OS: MacOS Mojave Version 10.14.5 (Darwin Mateuszs-MacBook-Pro.local 18.6.0 Darwin Kernel Version 18.6.0: Thu Apr 25 23:16:27 PDT 2019; root:xnu-4903.261.4~2/RELEASE_X86_64 x86_64)

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 1
  • Comments: 17 (10 by maintainers)

Commits related to this issue

Most upvoted comments

rule(:per_page).unless(:nil?).validate(gt?: 0, lteq?: 20)

:nil? looks ambiguous. It may mean key or value. :value? may be more suitable:

# for optional and maybe
rule(:per_page).if(:key?).if(:value?).validate(gt?: 0, lteq?: 20)

# for optional and maybe (combined if)
rule(:per_page).if(:key?, :value?).validate(gt?: 0, lteq?: 20)

:present? -> I’ve got mixed feelings about this

I know where it’s coming from. The spectre of ActiveModel validation haunts us all.

@musaffa I reported two feature requests:

It’s tentatively scheduled to be part of 1.7.0 release. I marked both as help wanted for the time being though. I’ll give this a shot during xmas break unless somebody beats me to it.

Maybe rule? instead of rule(...).if(:key?) as a shorthand?

for the record, this thing in PM is usually called “guard”, just to save some time on coming with proper naming which is, you know, hard…