dry-rb / dry-rb/dry-validation

Ractor incompatibility: contracts, schemas, types, and results cannot be used from non-main Ractors on Ruby 4.0

Open
#749 1 comment 0 reactions 0 assignees View on GitHub
bug help wanted
Dominant language
Ruby
Stars
1.4k
Forks
195
PR merge metrics
No merged PRs in 30d

Description

`dry-validation` (1.11.1) and its underlying dependencies (`dry-schema`, `dry-types`, `dry-initializer`) cannot be used inside [Ractors](https://docs.ruby-lang.org/en/master/Ractor.html) on Ruby 4.0. No operation works — instantiating contracts, calling them, defining schemas, looking up types, or even passing a validation result into a Ractor.

### Environment

- **Ruby version:** 4.0.3 (2026-04-21)
- **dry-validation:** 1.11.1
- **dry-schema:** 1.16.0
- **dry-types:** 1.9.1
- **dry-initializer:** 3.2.0
- **OS:** Linux x86_64

## Reproduction

Save as `dry_validation_ractor_poc.rb` and run with `ruby dry_validation_ractor_poc.rb`:

```ruby
#!/usr/bin/env ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile(true) do
source "https://rubygems.org"
gem "dry-validation", "~> 1.10"
end

require "dry-validation"

version = Gem.loaded_specs["dry-validation"]&.version
puts "Ruby #{RUBY_VERSION}, dry-validation #{version}"
puts "-" * 50

class UserContract < Dry::Validation::Contract
params do
required(:name).filled(:string)
required(:age).filled(:integer, gt?: 0)
end
rule(:name) { key.failure("too short") if value.length < 2 }
end

contract = UserContract.new

tests = {
"Contract.new in Ractor" => -> { Ractor.new { UserContract.new }.value },
"contract.call in Ractor" => -> { Ractor.new(contract) { |c| c.call(name: "Alice", age: 30) }.value },
"Define contract in Ractor" => -> {
Ractor.new {
Class.new(Dry::Validation::Contract) { params { required(:x).filled(:string) } }.new.call(x: "hi")
}.value
},
"Dry::Types[] in Ractor" => -> { Ractor.new { Dry::Types["strict.string"] }.value },
"Dry::Schema.Params in Ractor"=> -> { Ractor.new { Dry::Schema.Params { required(:a).filled(:string) } }.value },
"Pass result across Ractor" => -> { Ractor.new(contract.call(name: "Alice", age: 30)) { |r| r.success? }.value },
}

tests.each do |name, test|
print "#{name}: "
begin
test.call
puts "OK"
rescue Ractor::RemoteError => e
puts "FAILED — #{e.cause.class}: #{e.cause.message}"
rescue => e
puts "FAILED — #{e.class}: #{e.message}"
end
end
```

### Actual output

```
Ruby 4.0.3, dry-validation 1.11.1
--------------------------------------------------
Contract.new in Ractor: FAILED — RuntimeError: defined with an un-shareable Proc in a different Ractor
contract.call in Ractor: FAILED — TypeError: allocator undefined for Proc
Define contract in Ractor: FAILED — Ractor::IsolationError: can not get unshareable values from instance variables of classes/modules from non-main Ractors (@dry_initializer from Dry::Validation::Contract)
Dry::Types[] in Ractor: FAILED — Ractor::IsolationError: can not get unshareable values from instance variables of classes/modules from non-main Ractors (@type_map from Dry::Types)
Dry::Schema.Params in Ractor: FAILED — RuntimeError: defined with an un-shareable Proc in a different Ractor
Pass result across Ractor: FAILED — TypeError: allocator undefined for Proc
```

### Expected behavior

At minimum, pre-built contracts should be callable from non-main Ractors, and results should be transferable across Ractor boundaries. Ideally, defining new contracts and looking up types should also work.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.