Sub-nested arrays with schemas

gem: dry-validation
version: 0.10.7

Ok, so I’m trying to validate this structure:

portfolios: [
  {
    name: 'Portfolio 1',
    return_rates: [
      { date: '1998-02', rate: 0.05 },
      { date: '2016-12', rate: 0.012 }
    ]
  },
  {
    name: 'Portfolio 2',
    return_rates: [
      { date: '1998-02', rate: 0.06 },
      { date: '2016-12', rate: 0.04 }
    ]
  }
]

My schema looks like this:

# type_specs = true is enabled for JSON in an initializer

SCHEMA = Dry::Validation.JSON do
  required(:portfolios, :array).filled(min_size?: 1).each do 
    schema(PORTFOLIO_SCHEMA)
  end
end

PORTFOLIO_SCHEMA = Dry::Validation.JSON do
  required(:name, [:nil, :string]).filled(:str?)
  required(:return_rates, :array).filled(min_size?: 1).each do
    schema(PORTFOLIO_RETURN_RATE_SCHEMA)
  end
end

PORTFOLIO_RETURN_RATE_SCHEMA = Dry::Validation.JSON do
  required(:date, [:nil, App::Types::YearMonthDate]).filled(:date?)
  required(:rate, [:nil, :decimal]).filled(:decimal?)
end

This kind of works, the only problem is that if the input is like this:

portfolios: [
  {
    name: 'damn',
    return_rates: []
  }
]

No error is given, it seems like the rule filled(min_size?: 1) is not executed on PORTFOLIO_SCHEMA.
I’ve tried using each as a block in filled(min_size?: 1) { each { schema(PORTFOLIO_SCHEMA) } }, then the validation is executed, but no coercion happens on PORTFOLIO_RETURN_RATE_SCHEMA.

Am I doing something wrong or is this not achievable right now?

Each will not work for empty array. Because each will never be run for empty one. You need extra declaration before to exclude empty. If you don’t want them. Like:

filled?.each ...

@gotar thanks for your answer. But using filled?.each doesn’t work, the schema still passes if I send an empty array of portfolios or return_rates.

I’ve read in other answers that chaining methods is not supported, this might be the cause of it. That’s why I’ve tried to use filled { each }, even value { each } and so on, but none of them work as I needed, this time they do invalidate a schema with an empty array, but then the coercions are not executed, so the valid date in a year-month format inside portfolios are never coerced into Date and so it fails when trying to save the records.

Ok, I just tested something here and it worked, it seems like I need to create a complete coercion rule, like so:

required(
  :portfolios,
  [{ name: :string, return_rates: [{date: App::Types::YearMonthDate, rate: :decimal}] }]
).filled(:array?, min_size?: 1) do
  each do
    schema(PORTFOLIO_SCHEMA)
  end
end

It works, but is quite messy…