module MultiJSON

A unified interface for JSON libraries in Ruby

MultiJSON allows swapping between JSON backends without changing your code. It auto-detects available JSON libraries and uses the fastest one available.

## Method-definition patterns

The current public API uses two patterns, each chosen for a specific reason:

  1. “module_function“ creates both a class method and a private instance method from a single “def“. This is used for the hot-path API (“adapter“, “use“, “adapter=“, “parse“, “generate“, “current_adapter“) so that both “MultiJSON.parse(…)“ and legacy “Class.new { include MultiJSON }.new.send(:parse, …)“ invocations work through the same body. The instance versions are re-publicized below so YARD renders them as part of the public API.

  2. “def self.foo“ creates only a singleton method, giving mutation testing a single canonical definition to target. This is used for {.with_adapter}, which needs precise mutation coverage of its fiber-local save/restore logic.

Deprecated public API (“decode“, “encode“, “engine“, “load“, “dump“, etc.) lives in {file:lib/multi_json/deprecated.rb} so this file stays focused on the current surface.

@example Basic usage

MultiJSON.parse('{"foo":"bar"}')  #=> {"foo" => "bar"}
MultiJSON.generate({foo: "bar"})  #=> '{"foo":"bar"}'

@example Specifying an adapter

MultiJSON.use(:oj)
MultiJSON.parse('{"foo":"bar"}', adapter: :json_gem)

@api public

Public Class Methods

Sets the adapter to use for JSON operations

@api public @return [Class] the loaded adapter class @example

MultiJSON.adapter = :json_gem
Alias for: use

Resolve the “ParseError“ constant for an adapter class

The result is memoized on the adapter class itself in a “@_multi_json_parse_error“ ivar so subsequent “MultiJSON.load“ calls skip the constant lookup entirely. The lookup is performed with “inherit: false“ so a stray top-level “::ParseError“ constant in the host process is correctly ignored on every supported Ruby implementation — TruffleRuby’s “::“ operator walks the ancestor chain and would otherwise pick up the top-level constant. Custom adapters that don’t define their own “ParseError“ get a clear {AdapterError} instead of the bare “NameError“ Ruby would raise from the rescue clause.

@api private @param adapter_class [Class] adapter class to inspect @return [Class] the adapter’s ParseError class @raise [AdapterError] when the adapter doesn’t define ParseError

# File lib/multi_json.rb, line 100
def self.parse_error_class_for(adapter_class)
  cached = adapter_class.instance_variable_get(:@_multi_json_parse_error)
  return cached if cached

  resolved = adapter_class.const_get(:ParseError, false)
  adapter_class.instance_variable_set(:@_multi_json_parse_error, resolved)
rescue NameError
  raise AdapterError, "Adapter #{adapter_class} must define a ParseError constant"
end

Emit a deprecation warning at most once per process for the given key

Defined as a singleton method (rather than via module_function) so there is exactly one definition for mutation tests to target. Public so the deprecated “load_options“ / “dump_options“ aliases on the {Options} mixin can invoke it without routing through “MultiJSON.send(…)“.

The warning is tagged with the “:deprecated“ category so callers can silence the whole set with “Warning = false“ or surface it via “ruby -W:deprecated“ — the standard Ruby idiom for library deprecations since 2.7.

@api private @param key [Symbol] identifier for the deprecation (typically the method name) @param message [String] warning message to emit on first call @return [void] @example

MultiJSON.warn_deprecation_once(:foo, "MultiJSON.foo is deprecated")
# File lib/multi_json.rb, line 74
def self.warn_deprecation_once(key, message)
  Concurrency.synchronize(:deprecation_warnings) do
    return if DEPRECATION_WARNINGS_SHOWN.include?(key)

    Kernel.warn(message, category: :deprecated)
    DEPRECATION_WARNINGS_SHOWN.add(key)
  end
end

Executes a block using the specified adapter

Defined as a singleton method so mutation testing has exactly one definition to target. The override is stored in fiber-local storage so concurrent fibers and threads each see their own adapter without racing on a shared module variable; nested calls save and restore the previous fiber-local value.

@api public @param new_adapter [Symbol, String, Module] adapter to use @yield block to execute with the temporary adapter @return [Object] result of the block @example

MultiJSON.with_adapter(:json_gem) { MultiJSON.dump({}) }
# File lib/multi_json.rb, line 241
def self.with_adapter(new_adapter)
  previous_override = Fiber[:multi_json_adapter]
  Fiber[:multi_json_adapter] = load_adapter(new_adapter)
  yield
ensure
  Fiber[:multi_json_adapter] = previous_override
end

Public Instance Methods

Returns the current adapter class

Honors a fiber-local override set by {.with_adapter} so concurrent blocks observe their own adapter without clobbering the process-wide default. Falls back to the process default when no override is set.

@api public @return [Class] the current adapter class @example

MultiJSON.adapter  #=> MultiJSON::Adapters::Oj
# File lib/multi_json.rb, line 127
def adapter
  override = Fiber[:multi_json_adapter]
  return override if override

  @adapter ||= use(nil)
end

Sets the adapter to use for JSON operations

@api public @return [Class] the loaded adapter class @example

MultiJSON.adapter = :json_gem
Alias for: use

Returns the adapter to use for the given options

“nil“ is accepted as a no-options sentinel — explicit “current_adapter(nil)“ calls fall through to the process default adapter without raising.

@api public @param options [Hash, nil] options that may contain :adapter key, or

nil to use the process default

@return [Class] adapter class @example

MultiJSON.current_adapter(adapter: :oj)  #=> MultiJSON::Adapters::Oj
# File lib/multi_json.rb, line 200
def current_adapter(options = {})
  options ||= Options::EMPTY_OPTIONS
  adapter_override = options[:adapter]
  adapter_override ? load_adapter(adapter_override) : adapter
end

Serializes a Ruby object to a JSON string

@api public @param object [Object] object to serialize @param options [Hash] serialization options (adapter-specific) @return [String] JSON string @example

MultiJSON.generate({foo: "bar"})  #=> '{"foo":"bar"}'
# File lib/multi_json.rb, line 214
def generate(object, options = {})
  current_adapter(options).dump(object, options)
end

Parses a JSON string into a Ruby object

Returns “nil“ for “nil“, empty, and whitespace-only inputs instead of raising. Pass an explicit non-blank string if you want to surface a {ParseError} for empty payloads at the call site.

@api public @param string [String, read] JSON string or IO-like object @param options [Hash] parsing options (adapter-specific) @return [Object, nil] parsed Ruby object, or nil for blank input @raise [ParseError] if parsing fails @raise [AdapterError] if the adapter doesn’t define a “ParseError“ constant @example

MultiJSON.parse('{"foo":"bar"}')  #=> {"foo" => "bar"}
MultiJSON.parse("")               #=> nil
MultiJSON.parse("   \n")          #=> nil
# File lib/multi_json.rb, line 178
def parse(string, options = {})
  adapter_class = current_adapter(options)
  parse_error_class = MultiJSON.parse_error_class_for(adapter_class)
  begin
    adapter_class.load(string, options)
  rescue parse_error_class => e
    raise ParseError.build(e, string)
  end
end

Sets the adapter to use for JSON operations

The merged-options cache is only reset when the new adapter loads successfully. A failed “use(:nonexistent)“ leaves the cache in place so the previously-active adapter keeps its cached entries.

@api public @param new_adapter [Symbol, String, Module, nil] adapter specification @return [Class] the loaded adapter class @example

MultiJSON.use(:oj)
# File lib/multi_json.rb, line 145
def use(new_adapter)
  loaded = load_adapter(new_adapter)
  Concurrency.synchronize(:adapter) do
    OptionsCache.reset
    @adapter = loaded
  end
end
Also aliased as: adapter=