`validate_keys` is not respected within nested hashes
- Dominant language
- Ruby
- Stars
- 492
- Forks
- 123
- PR merge metrics
- No merged PRs in 30d
Description
## Describe the bug
When defining a schema whose keys should be validated with the exception of one field, modifying the `config.validate_keys` value within the child field seems to apply the change to the root schema as well.
Additionally, depending on the order in which you define the sub fields, you may encounter a cryptic `FrozenConfigError`.
## To Reproduce
```rb
# Keys in this schema should be validated
schema = Dry::Schema.JSON do
config.validate_keys = true
# EXCEPT for keys under the `unvalidated` key
required(:unvalidated).hash do
config.validate_keys = false
end
required(:validated).hash do
required(:foo).filled(:string)
end
end
# This results in no errors even though `validated.biz` is not valid according to the schema
validation = schema.call({
unvalidated: { foo: "bar" },
validated: {
foo: "bar",
biz: "baz",
},
})
```
Additionally, if you reorder the `hash` blocks, then you get a `FrozenConfigError`. For example:
```rb
schema = Dry::Schema.JSON do
config.validate_keys = true
required(:validated).hash do
required(:foo).filled(:string)
end
# EXCEPT for keys under the `unvalidated` key
required(:unvalidated).hash do
# Frozen config error here
config.validate_keys = false
end
end
```
Slightly different, but related. If you define a separate stand-alone schema for the unvalidated key, the `config.validate_keys = false` is completely ignored. This is again at best surprising and at worst a serious bug.
```rb
UnvalidatedSchema = Dry::Schema.JSON do
config.validate_keys = false
end
# This behaves as if `config.validate_keys = true` for `UnvalidatedSchema`
Schema = Dry::Schema.JSON do
config.validate_keys = true
required(:unvalidated).hash(UnvalidatedSchema)
end
# Has error for unknown keys
validation = Schema.call({ unvalidated: { foo: "bar" }})
```
## Expected behavior
Either:
1. The `config` should be scoped to the current schema definition and not implicitly modify parent configurations at a minimum. Configuration implicitly propagating down is reasonable, but child configs modifying parents is surprising.
2. Make modifying configuration in children impossible regardless of order. If you don't want this to be configurable, it shouldn't allow the use to do this at all.
In my opinion, #2 is a no-go, because it is extra confusing when you compose schemas as shown in the last example above. The configuration in the composed schema is just completely ignored.
## My environment
- Ruby version:
```
$ ruby --version
ruby 3.4.7 (2025-10-08 revision 7a5688e2a2) +PRISM [arm64-darwin25]
```
- OS:
```
$ sw_vers
ProductName: macOS
ProductVersion: 26.2
BuildVersion: 25C56
```
Contributor guide
Assessment
This issue has not been assessed yet.