dry-rb / dry-rb/dry-validation

Failed to coerce a property when at least one predicates in a nested hash do not meet validation rules.

Open
#741 1 comment 0 reactions 0 assignees View on GitHub
bug help wanted
Dominant language
Ruby
Stars
1.4k
Forks
195
PR merge metrics
No merged PRs in 30d

Description

## Describe the bug

When dealing with predicates that are nested within a hash, either they all succeed or all fail.

## To Reproduce

```ruby
# frozen_string_literal: true

require 'bundler/inline'

gemfile do
source 'https://rubygems.org'

gem 'dry-validation'
gem 'rspec'
end

require 'rspec/autorun'

class RecurringTransferContract < Dry::Validation::Contract
DATE_ISO8601_REGEX = /\A\d{4}-\d{2}-\d{2}\z/

params do
required(:recurring_transfer).hash do
required(:first_execution_date).filter(format?: DATE_ISO8601_REGEX).value(:date)
optional(:last_execution_date).filter(format?: DATE_ISO8601_REGEX).value(:date)
end
end

rule(recurring_transfer: :first_execution_date) do
key.failure({code: "i_am_a_string", text: "It is a string instead of a date"}) if value.is_a?(String)
end

RSpec.describe RecurringTransferContract do
it "succeeds when both dates are valid" do
contract = described_class.new.call(recurring_transfer: {first_execution_date: "2025-01-01", last_execution_date: "2025-11-03"})

expect(contract).to be_success
end

it "raises an error when last_execution_date is not passing the filter predicate" do
contract = described_class.new.call(recurring_transfer: {first_execution_date: "2025-01-01", last_execution_date: ""})

expect(contract).to be_failure
expect(contract.errors.to_h[:recurring_transfer]).to eq(
{
first_execution_date: [{code: "i_am_a_string", text: "It is a string instead of a date"}],
last_execution_date: ["is in invalid format"],
}
)
end
end
end
```

Running the test above fails because of `first_execution_date: [{text: "It is a string instead of a date", code: "i_am_a_string"}]`

## Expected behavior

My expectation would be that `first_execution_date` is always casted to a `Date` since it is passing the filter predicate.

However, if you you move the predicates to the top level, it works as expected

```ruby
# frozen_string_literal: true

require 'bundler/inline'

gemfile do
source 'https://rubygems.org'

gem 'dry-validation'
gem 'rspec'
end

require 'rspec/autorun'

class RecurringTransferContract < Dry::Validation::Contract
DATE_ISO8601_REGEX = /\A\d{4}-\d{2}-\d{2}\z/

params do
required(:first_execution_date).filter(format?: DATE_ISO8601_REGEX).value(:date)
optional(:last_execution_date).filter(format?: DATE_ISO8601_REGEX).value(:date)
end

rule(:first_execution_date) do
key.failure({code: "i_am_a_string", text: "It is a string instead of a date"}) if value.is_a?(String)
end

RSpec.describe RecurringTransferContract do
it "succeeds when both dates are valid" do
contract = described_class.new.call({first_execution_date: "2025-01-01", last_execution_date: "2025-11-03"})

expect(contract).to be_success
end

it "raises an error when last_execution_date is not passing the filter predicate" do
contract = described_class.new.call({first_execution_date: "2025-01-01", last_execution_date: ""})

expect(contract).to be_failure
expect(contract.errors.to_h).to eq(
{
last_execution_date: ["is in invalid format"]
}
)
end
end
end
```

## My environment

- Affects my production application: **YES**
- Ruby version: `ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23]`
- OS: `MacOS`

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.