dry-rb/dry-validation

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

Open

#749 opened on May 8, 2026

 (1 comment) (0 reactions) (0 assignees)Ruby (196 forks)github user discovery
bughelp wanted

Repository metrics

Stars
 (1,422 stars)
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 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:

#!/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