Skip to content

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

Open
@PericlesTheo

Description

@PericlesTheo

Describe the bug

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

To Reproduce

# 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

# 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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions