dry-schema
dry-schema copied to clipboard
Schema with dynamic keys
Hi,
Are there any options to describe schema with dynamic keys?
Let's say I have a hash where keys are string numbers:
{
foo: {
'1' => { name: 'bar1' },
'2' => { name: 'bar2' },
...
'100' => { name: 'bar100' }
}
}
For now I can write a schema by using some limited predefined keys:
Dry::Validation.Schema do
required(:foo).schema do
('1'..'100').each do |position|
optional(position).schema do
required(:name).filled(:str?)
end
end
end
end
But this approach is not that flexible in case if I need to accept keys which may be any number.
Is using imperative custom validation blocks the only option to write flexible validation in this case?
BarSchema = Dry::Validation.Schema do
required(:name).filled(:str?)
end
schema = Dry::Validation.Schema do
required(:foo).filled
configure do
def self.messages
# How can I get detailed errors here from BarSchema?
super.merge(en: { errors: { valid_foo: 'foo is not valid' } })
end
end
validate(valid_foo: :foo) do |key_values|
key_values.all? do |k, v|
k ~= /\A\d+\z/ && BarSchema.call(v).success?
end
end
end
Not sure, but maybe allowing to write some custom key matchers would be useful in this case. For example:
REGEX = /\A\d+\z/
Dry::Validation.Schema do
matcher(:string_number) { |key| key ~= REGEX }
required(:foo).schema do
match(:string_number).schema do
required(:name).filled(:str?)
end
end
end
Or something like:
Dry::Validation.Schema do
required(:foo).schema do
optional(REGEX).schema do
required(:name).filled(:str?)
end
end
end
This feature is planned for 1.0.0
Hi, we are just thinking about using dry-validate in our project. Something custom written is in place right now. We face the same problem and have dynamic keys at some places. Any idea when Version 1.0.0 and this feature will be ready?
👍 For this feature
Is there any likelihood of this feature being selected for development soon?
@jphastings we want to support this in 1.0.0, I hope to start working on it next month.
@solnic any update on this?
@kbredemeier it'll be supported in dry-validation 1.0.0, as mentioned, it'll be done later this year, hopefully before the summer
@solnic any news here?
@anthonydmitriyev unfortunately, I'm still working on dry-schema. It'll be ready when it's ready. Stay tuned.
Any update on this @solnic ?
No update on this front, @jacobtani, nor any immediate plans AFAIK.
You can be sure that we'll update or close this issue when we've done any related work.
@solnic it would be fantastic to have a solution for this. Keep running into this again and again when validating fields coming from Rails' nested_attributes, which come in the format:
{ id => { … }, id => { … } }
If there's a temporary work-around or any solution I can help implement, please do let me know.
Thank you!
@tomasc we'll add it eventually, I'm now focusing on rom/hanami2.0 so I don't have time for dry-validation/schema. It is what it is 🤷🏻
There is no workaround other than to use dry-validation without specifying keys and using custom rules to validate things manually. In order to have this feature done properly, we need a new rule type in dry-logic and then adding support for it here. I wouldn't be surprised if it turns out to be a simple addition at the end of the day 😅
@solnic thanks for getting back to me. I see … and understand ;-). Pity though, this seems to be the last piece of the puzzle (at least for me) that would cover all use cases when using dry-validation with Rails / form objects / contracts.
The issue I had specifically was that I use contract schemas with dry-rails. And since the nested Hashes are not being processed by the nested Param schemas, I am running into issues.
My workaround is to pre-process the params manually by looping through the nested hashes, applying the schemas that process the attributes, and re-adding the processed hashes back, before passing them onto the safe_params. Not the most beautiful thing, but gets the work done.
@tomasc I currently have dry-validation in a rails app at work, so it is very likely that I will need this feature soon too. If this happens, I'll most likely just go ahead and implement it 🤞🏻
Hopefully, @solnic doesn't mind me quoting him here, but I asked him a while back what he thought it would take to support this, and this is his answer. I had intended on looking into implementing it myself, but I haven't had time. Maybe someone else does, and this answer is the key to helping them.
I know how to do it and what's required. We need a new operation sub-type in dry-logic that would work like Dry::Logic::Operation::Key but instead of extracting a value found under a specific path (a key is actually a path to a value), it would extract one or more values found under paths matching a regex pattern. Then we'll need to come up with an AST node that would represent a dynamic key and add support for it to the Dry::Logic::RuleCompiler and THEN we'll need to extend schema's DSL to support dynamic key syntax, ie required(/a-z+/).value(:string)
@adam12 I don't mind 🙂 This hasn't changed btw, it's still what needs to be done (more or less)
I have keys that are random UUID's. (Basically a JSON object acting as a Dictionary.) Is there even an ugly workaround? Or just SOL since it cannot be defined as part of the AST?
You can pre-transform input like
transformed = {
values: input.to_h { |key, value| { key: key, value: value } }
}
Schema = Dry::Schema.JSON do
required(:values).array(:hash) do
required(:key).value(:string)
required(:value).value(...)
end
end
It's enough to check if input is a Hash before, otherwise it's a safe transformation.
I've also had this issue when using Rails nested_attributes. My not so elegant solution was to use dry-validation and a macro:
register_macro(:nested_schema) do |macro:|
if value.present?
params = macro.args[0]
values[key_name].each do |attr_key, attr_value|
res = params.call(attr_value)
next if res.success?
res.errors.each do |m|
key([key_name, attr_key, m.path&.first]).failure(m.text)
end
end
end
end
A rule based on the macro:
rule(:foo_attributes).validate(
nested_schema: Dry::Schema.Params do
required(:fu).filled(:string)
required(:bar).filled(:string,
included_in?: BAR_CODES)
end
)
And finaly a schema entry:
optional(:foo_attributes).value(:hash)