Hi there.
I’m pretty new to the Dry set of libraries (have been exploring the docs/playing around with it during the last few days and started on a basic POC). Just to provide some background to my use-case, I maintain a Gem that wraps a REST API, and want to add a validation layer to the gem. Dry Schema/Validation seems like great libraries and I think they could be a good fit for what I want to do. I have most of it worked out, but have hit a bit of a dead-end with one specific part of the implementation – arrays of custom types.
A somewhat contrived example for the type of data that I need to validate might look something like this:
person = {
name: 'Bob',
age: 19,
contact_details: {
email: 'bob@example.org',
phone: '1234567890',
address: {
street: '123 Main St',
city: 'Anytown',
zipcode: '12345'
},
favorite_things: [
{ type: 'Color', name: 'red', code: 1 },
{ type: 'Fruit', name: 'apple' },
{ type: 'Book', title: 'The Great Gatsby', genre: 'Fiction' }
]
}
}
Most of the validation objects and schemas for this are fairly straightforward, but I’m struggling with the array of hashes. I thought I could perhaps use custom types for the various Hash objects, but validation only seems to work if the array is of a single custom type, not multiple custom types. However, multiple standard types does validate correctly.
Examples
Array of multiple standard types (e.g. Integer, String) works as expected.
require 'dry-validation'
require 'dry-schema'
class PersonContract < Dry::Validation::Contract
schema do
required(:name).filled(:string)
required(:age).value(:integer)
optional(:favorite_things).value(array[Types::Integer | Types::String])
end
end
# Valid inputs:
valid_person = {
name: 'Bob',
age: 19,
favorite_things: [
1,
'hello'
]
}
valid_person_result = person_contract.call(valid_person)
# valid_person_result.success? => true
# Inalid inputs:
invalid_person = {
name: 'Bob',
age: 19,
favorite_things: [
1,
'hello',
:foo # This should fail validation since :foo is not an Integer or String
]
}
invalid_person_result = person_contract.call(invalid_person)
# invalid_person_result.success? => false
# invalid_person_result.errors.to_h
# => {favorite_things: {2 => ["must be an integer or must be a string"]}}
Array of single custom type works as expected
require 'dry-validation'
require 'dry-schema'
require 'dry-types'
Types = Dry.Types()
Color = Types::Hash.schema(type: Types::String, name: Types::String, code: Types::Integer)
class PersonContract < Dry::Validation::Contract
schema do
required(:name).filled(:string)
required(:age).value(:integer)
optional(:favorite_things).value(array[Color])
end
end
person_contract = PersonContract.new
# Valid inputs:
valid_person = {
name: 'Bob',
age: 19,
favorite_things: [
{ type: 'Color', name: 'red', code: 1 },
{ type: 'Color', name: 'blue', code: 2 }
]
}
valid_person_result = person_contract.call(valid_person)
# valid_person_result.success? => true
# Inalid inputs:
invalid_person = {
name: 'Bob',
age: 19,
favorite_things: [
{ type: 'Color', name: 'red', code: 1 },
{ type: 'Color', name: 'blue' }, # This should fail validation since it is missing the code key
{ type: 'Color', name: 123 } # This should fail validation since name is not a String and it is missing the code key
]
}
invalid_person_result = person_contract.call(invalid_person)
# invalid_person_result.success? => false
# invalid_person_result.errors.to_h
# => {favorite_things: {1 => {code: ["is missing"]}, 2 => {name: ["must be a string"], code: ["is missing"]}}}
Array of multiple custom type does not work as expected
require 'dry-validation'
require 'dry-schema'
require 'dry-types'
Types = Dry.Types()
Color = Types::Hash.schema(type: Types::String, name: Types::String, code: Types::Integer)
Book = Types::Hash.schema(type: Types::String, title: Types::String, genre: Types::String)
class PersonContract < Dry::Validation::Contract
schema do
required(:name).filled(:string)
required(:age).value(:integer)
optional(:favorite_things).value(array[Color | Book])
end
end
person_contract = PersonContract.new
# Valid inputs:
valid_person = {
name: 'Bob',
age: 19,
favorite_things: [
{ type: 'Color', name: 'red', code: 1 },
{ type: 'Book', title: 'The Great Gatsby', genre: 'Fiction' }
]
}
valid_person_result = person_contract.call(valid_person)
# valid_person_result.success? => true
# Inalid inputs:
invalid_person = {
name: 'Bob',
age: 19,
favorite_things: [
{ type: 'Color', name: 'red', code: 1 },
{ type: 'Book', title: 'The Great Gatsby', genre: 'Fiction' },
{ type: 'Foo', foo: 'bar' } # This should fail validation since it does not match either Color or Book types
]
}
invalid_person_result = person_contract.call(invalid_person)
# invalid_person_result.success? => true
# invalid_person_result.errors.to_h
# => {}
I’m not sure if this is due to:
- User error: I’m doing something wrong in my implementation
- A limitation of the library for which I will need to find a custom work-around
- A bug
If it’s 1, I would very much appreciate some guidance from those more experienced with the Dry libraries as to where I am going wrong.
If it’s 2, I’m wondering if anyone has come up against this issue before, and if so how did you get around it? I’m thinking I might need to write a custom rule in a validation specifically for dealing with this type of array, or maybe look at defining some sort of macro (though I’ve not explored macros deeply yet, so this is just guesswork).
If it’s 3, please let me know. I’d be happy to open an issue on the GitHub repo for a bug report.
Thanks in advance.
Karl