From 77292cbc9b4b63248caaaff0fa2973b53e1eb9cd Mon Sep 17 00:00:00 2001 From: Robin Miller Date: Mon, 1 Sep 2025 22:40:34 -0600 Subject: [PATCH 1/2] Added testing for JSON.unsafe_load. Fixes NoMethodError when passing proc to JSON.unsafe_load, matching the changes made in 73d2137fd3ad18e2a524553e80531f434cf26dde. --- CHANGES.md | 1 + lib/json/common.rb | 10 ++- test/json/json_common_interface_test.rb | 81 +++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 779dd3ef7..14c770606 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,7 @@ ``` * Fix `JSON.generate` `strict: true` mode to also restrict hash keys. * Fix `JSON::Coder` to also invoke block for hash keys that aren't strings nor symbols. +* Fix `JSON.unsafe_load` usage with proc ### 2025-07-28 (2.13.2) diff --git a/lib/json/common.rb b/lib/json/common.rb index 45200a83b..f8aefcd28 100644 --- a/lib/json/common.rb +++ b/lib/json/common.rb @@ -703,9 +703,13 @@ def unsafe_load(source, proc = nil, options = nil) if opts[:allow_blank] && (source.nil? || source.empty?) source = 'null' end - result = parse(source, opts) - recurse_proc(result, &proc) if proc - result + + if proc + opts = opts.dup + opts[:on_load] = proc.to_proc + end + + parse(source, opts) end # :call-seq: diff --git a/test/json/json_common_interface_test.rb b/test/json/json_common_interface_test.rb index 745400faa..03e152265 100644 --- a/test/json/json_common_interface_test.rb +++ b/test/json/json_common_interface_test.rb @@ -162,6 +162,87 @@ def test_load_null assert_raise(JSON::ParserError) { JSON.load('', nil, :allow_blank => false) } end + def test_unsafe_load + string_able_klass = Class.new do + def initialize(str) + @str = str + end + + def to_str + @str + end + end + + io_able_klass = Class.new do + def initialize(str) + @str = str + end + + def to_io + StringIO.new(@str) + end + end + + assert_equal @hash, JSON.unsafe_load(@json) + tempfile = Tempfile.open('@json') + tempfile.write @json + tempfile.rewind + assert_equal @hash, JSON.unsafe_load(tempfile) + stringio = StringIO.new(@json) + stringio.rewind + assert_equal @hash, JSON.unsafe_load(stringio) + string_able = string_able_klass.new(@json) + assert_equal @hash, JSON.unsafe_load(string_able) + io_able = io_able_klass.new(@json) + assert_equal @hash, JSON.unsafe_load(io_able) + assert_equal nil, JSON.unsafe_load(nil) + assert_equal nil, JSON.unsafe_load('') + ensure + tempfile.close! + end + + def test_unsafe_load_with_proc + visited = [] + JSON.unsafe_load('{"foo": [1, 2, 3], "bar": {"baz": "plop"}}', proc { |o| visited << JSON.dump(o); o }) + + expected = [ + '"foo"', + '1', + '2', + '3', + '[1,2,3]', + '"bar"', + '"baz"', + '"plop"', + '{"baz":"plop"}', + '{"foo":[1,2,3],"bar":{"baz":"plop"}}', + ] + assert_equal expected, visited + end + + def test_unsafe_load_default_options + too_deep = '[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[["Too deep"]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]' + assert JSON.unsafe_load(too_deep, nil).is_a?(Array) + nan_json = '{ "foo": NaN }' + assert JSON.unsafe_load(nan_json, nil)['foo'].nan? + assert_equal nil, JSON.unsafe_load(nil, nil) + t = Time.now + assert_equal t, JSON.unsafe_load(JSON(t)) + end + + def test_unsafe_load_with_options + nan_json = '{ "foo": NaN }' + assert_raise(JSON::ParserError) { JSON.unsafe_load(nan_json, nil, :allow_nan => false)['foo'].nan? } + # make sure it still uses the defaults when something is provided + assert JSON.unsafe_load(nan_json, nil, :allow_blank => true)['foo'].nan? + end + + def test_unsafe_load_null + assert_equal nil, JSON.unsafe_load(nil, nil, :allow_blank => true) + assert_raise(TypeError) { JSON.unsafe_load(nil, nil, :allow_blank => false) } + assert_raise(JSON::ParserError) { JSON.unsafe_load('', nil, :allow_blank => false) } + end + def test_dump too_deep = '[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]' obj = eval(too_deep) From 92654cd99bdacb6b5a47f8c8b2a0bf3c4bbc08f6 Mon Sep 17 00:00:00 2001 From: Robin Miller Date: Mon, 1 Sep 2025 22:52:16 -0600 Subject: [PATCH 2/2] Update method docs for JSON.load and JSON.unsafe_load to show the correct use of proc argument. --- lib/json/common.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/json/common.rb b/lib/json/common.rb index f8aefcd28..ae9b787a0 100644 --- a/lib/json/common.rb +++ b/lib/json/common.rb @@ -662,6 +662,7 @@ def pretty_generate(obj, opts = nil) # when Array # obj.map! {|v| deserialize_obj v } # end + # obj # }) # pp ruby # Output: @@ -826,6 +827,7 @@ def unsafe_load(source, proc = nil, options = nil) # when Array # obj.map! {|v| deserialize_obj v } # end + # obj # }) # pp ruby # Output: