Skip to content

MultiJson backward-compat stub breaks method(:load) and silently resolves to Kernel#load` #226

@david-a-wheeler

Description

@david-a-wheeler

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!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions