How to define nested array of objects with validation and map them

I’m struggling to define the validation/schema/types (using dry-validation rc3) that:

  • takes a nested field (along with the top level ones): { line_items: [{ quantity: 1, code: 'ABC' }] }
  • applies the custom validation to each item (quantity can only be 1 if code is ABC)
  • maps the resulting (valid) hash to a Types::Array.of(LineItemType) (where LineItemType is similar to LineItemType = Types::Constructor(::LineItem)

I’ve struggled to find the examples in the documentation with not much success (no docs for v1 by the look of it?). I also got lost trying to look it up in the codebase.

For example, the input:

{
  "discount": 123,
  "line_items": [
    { "quantity": 1, "code": "ABC" }
  ]
}

Would need to be mapped (after the validations) into a Dry::Type-ed object:

Order.new(
  discount: 123,
  line_items: [LineItem.new(quantity: 1, code 'ABC')]
)

Would appreciate some help.

Cheers,
D.

Both dry-schema and dry-validation are not meant to be used for instantiating custom objects.

Why not just do this:

require 'dry/validation'
require 'dry/types'
require 'dry/struct'

module Types
  include Dry::Types()
end

class OrderContract < Dry::Validation::Contract
  params do
    required(:discount).value(:integer)

    required(:line_items).array(:hash) do
      required(:quantity).value(:integer)
      required(:code).value(:string)
    end
  end

  rule(:line_items) do
    values[:line_items].each_with_index do |item, idx|
      key([:line_items, idx]).failure('quantity must be 1') if item[:quantity] != 1 && item[:code] == 'ABC'
    end
  end
end

class LineItem < Dry::Struct
  attribute :quantity, Types::Integer
  attribute :code, Types::String
end

class Order < Dry::Struct
  attribute :discount, Types::Integer
  attribute :line_items, Types::Array.of(LineItem)
end

contract = OrderContract.new

data = {
  discount: 4,
  line_items: [
    { quantity: 2, code: 'FOO' },
    { quantity: 3, code: 'ABC' },
    { quantity: 4, code: 'BAR' }
  ]
}

result = contract.(data)

puts result.errors.to_h.inspect
# {:line_items=>{1=>["quantity must be 1"]}}

data = {
  discount: 4,
  line_items: [
    { quantity: 2, code: 'FOO' },
    { quantity: 1, code: 'ABC' },
    { quantity: 4, code: 'BAR' }
  ]
}

result = contract.(data)

puts result.errors.to_h.inspect
# {}

order = Order.new(data)

puts order.inspect
# #<Order discount=4 line_items=[#<LineItem quantity=2 code="FOO">, #<LineItem quantity=1 code="ABC">, #<LineItem quantity=4 code="BAR">]>

:question: :question:

Thanks @solnic. That would be fine.

How would you be able to validate the nested values though (quantity can only be 1 if the code=='ABC')? The rule doesn’t seem to exist on the nested schema.


Side note…

Not sure if that helps - maybe worth adding to the docs that custom objects instantiation is not supported?

I was under impression that it was ok because the values can be coerced using the Types::Constructor(Tod::TimeOfDay) { |s| Tod::TimeOfDay.parse(s) }.

So it is surprising for me that I cannot compose Array.of with Constructor.
Would help me massively If I somehow knew earlier (another option might be raising an exception when composing unsupported types during the validation).

Not sure if that makes sense for you though :pray:

I updated the above example to be complete and working. Also, here’s a gist with the same code dry_validation_and_struct.rb · GitHub

Yes this is a good suggestion. I’m working on new docs so I’ll make sure to include this information.

I can understand why it confused you. The reason why it’s not supported is that the whole idea behind this style of validation is to validate “pure data” (so, primitive objects, like hashes, arrays, strings, numbers etc.) before the data can be passed down to your system. Instantiating custom objects prior validation is just against this idea BUT it is clearly not enforced by the library in any way, which is the actual problem we’re having here.

I need to think about it because it’s not immediately obvious how to deal with it :slight_smile: I’ll figure it out eventually. Thanks for feedback!

1 Like