MultiJson backward-compat stub breaks method(:load) and silently resolves to Kernel#load
Summary
multi_json 1.21.0 introduced a MultiJson (mixed-case) backward-compatibility stub that forwards method calls via method_missing. However, Ruby's Object#method does not invoke method_missing, so MultiJson.method(:load) silently resolves to Kernel#load (the Ruby file-loader) rather than MultiJSON.load (the JSON parser). Any library that stores a multi_json method as a cached callable is silently broken in 1.21.0. An example is the sawyer library, which fails when multi_json is updated.
Affected versions
- Broken: multi_json 1.21.0
- Working: multi_json 1.20.1 and earlier
Reproduction
require 'multi_json'
# Works in both 1.20.1 and 1.21.0 \u2014 method_missing forwards the call
MultiJson.load('{"a":1}') # => {"a" => 1}
# Broken in 1.21.0 \u2014 method() does not invoke method_missing
m = MultiJson.method(:load)
puts m.inspect # 1.20.1: #<Method: MultiJson.load>
# 1.21.0: #<Method: Kernel#load> \u2190 wrong!
m.call('{"a":1}') # 1.21.0: LoadError: cannot load such file -- {"a":1}
Root cause
In 1.21.0, MultiJson became a stub module whose singleton class defines only method_missing and respond_to_missing?. Because Object#method resolves methods via the normal ancestor chain \u2014 it does not call method_missing \u2014 the lookup for :load finds no singleton method on MultiJson, walks up the ancestors through Module, Object, and into Kernel, and returns Kernel#load (the file-loader). The result is a Method object pointing at the wrong callable, with no warning at retrieval time.
The failure is catastrophic: code that caches MultiJson.method(:load) and later calls it with JSON data passes the entire JSON string to Kernel#load as a file path, producing a LoadError whose message is the raw JSON body.
Impact
Any library that stores a multi_json method as a callable is affected. The known case is Sawyer 0.9.3 (used by Octokit), which does this in Sawyer::Serializer#initialize:
@load = @format.method(load_method_name || :load)
With @format = MultiJson, @load silently becomes Kernel#load in 1.21.0. Every GitHub API response body then gets passed to Kernel.load as a Ruby file path, causing a LoadError in all tests that use VCR-recorded GitHub API responses. The relevant backtrace frame is:
sawyer-0.9.3/lib/sawyer/serializer.rb:64:in 'Kernel#load'
sawyer-0.9.3/lib/sawyer/serializer.rb:64:in 'Method#call'
sawyer-0.9.3/lib/sawyer/serializer.rb:64:in 'Sawyer::Serializer#decode'
Suggested fix
Define real singleton methods on the MultiJson stub for each forwarded name. method_missing is not sufficient for a true drop-in replacement; method(:name) must also return the correct callable.
module MultiJson
class << self
def load(*args, **kwargs, &block)
warn_once
::MultiJSON.parse(*args, **kwargs, &block)
end
def dump(*args, **kwargs, &block)
warn_once
::MultiJSON.generate(*args, **kwargs, &block)
end
# \u2026likewise for decode, encode, etc.
private
def warn_once
::MultiJSON.warn_deprecation_once(
:multi_json_constant,
"The MultiJson constant is deprecated and will be removed in v2.0. Use MultiJSON instead."
)
end
end
end
This makes MultiJson.method(:load) return a Method bound to MultiJson.load (which delegates to MultiJSON.parse), restoring the pre-1.21.0 behavior for any caller that caches methods as callables.
An alternative: rather than individual delegation methods, expose a method_missing-aware method override. This is possible but more complex and harder to reason about.
Workaround
Pin multi_json to < 1.21.0 in your Gemfile until this is resolved:
gem 'multi_json', '< 1.21.0'
Credit
Once I found that sawyer failed, I asked Claude Code to determine the
cause (text shown above, edited by me the human). I believe it's correct. It's certainly
correct that pinning multi_json to < 1.21.0 fixes the problem.
Thanks!
MultiJsonbackward-compat stub breaksmethod(:load)and silently resolves toKernel#loadSummary
multi_json1.21.0 introduced aMultiJson(mixed-case) backward-compatibility stub that forwards method calls viamethod_missing. However, Ruby'sObject#methoddoes not invokemethod_missing, soMultiJson.method(:load)silently resolves toKernel#load(the Ruby file-loader) rather thanMultiJSON.load(the JSON parser). Any library that stores a multi_json method as a cached callable is silently broken in 1.21.0. An example is the sawyer library, which fails whenmulti_jsonis updated.Affected versions
Reproduction
Root cause
In 1.21.0,
MultiJsonbecame a stub module whose singleton class defines onlymethod_missingandrespond_to_missing?. BecauseObject#methodresolves methods via the normal ancestor chain \u2014 it does not callmethod_missing\u2014 the lookup for:loadfinds no singleton method onMultiJson, walks up the ancestors throughModule,Object, and intoKernel, and returnsKernel#load(the file-loader). The result is aMethodobject pointing at the wrong callable, with no warning at retrieval time.The failure is catastrophic: code that caches
MultiJson.method(:load)and later calls it with JSON data passes the entire JSON string toKernel#loadas a file path, producing aLoadErrorwhose message is the raw JSON body.Impact
Any library that stores a multi_json method as a callable is affected. The known case is Sawyer 0.9.3 (used by Octokit), which does this in
Sawyer::Serializer#initialize:With
@format = MultiJson,@loadsilently becomesKernel#loadin 1.21.0. Every GitHub API response body then gets passed toKernel.loadas a Ruby file path, causing aLoadErrorin all tests that use VCR-recorded GitHub API responses. The relevant backtrace frame is:Suggested fix
Define real singleton methods on the
MultiJsonstub for each forwarded name.method_missingis not sufficient for a true drop-in replacement;method(:name)must also return the correct callable.This makes
MultiJson.method(:load)return aMethodbound toMultiJson.load(which delegates toMultiJSON.parse), restoring the pre-1.21.0 behavior for any caller that caches methods as callables.An alternative: rather than individual delegation methods, expose a
method_missing-awaremethodoverride. This is possible but more complex and harder to reason about.Workaround
Pin
multi_jsonto< 1.21.0in yourGemfileuntil this is resolved:Credit
Once I found that sawyer failed, I asked Claude Code to determine the
cause (text shown above, edited by me the human). I believe it's correct. It's certainly
correct that pinning
multi_jsonto< 1.21.0fixes the problem.Thanks!