diff --git a/CHANGES.md b/CHANGES.md index 079fd0c49..f405db416 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,7 @@ ### Unreleased * Fix `JSON::ParserError#json_path` raising `NoMethodError` when the document was parsed with `symbolize_names: true`. +* Fix `JSON.generate` not detecting duplicate keys such as `{ "1" => 1, 1 => 2 }`. ### 2026-10-13 (3.1.0) diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c index 38c88f231..87da10027 100644 --- a/ext/json/ext/generator/generator.c +++ b/ext/json/ext/generator/generator.c @@ -1020,6 +1020,9 @@ json_object_i(VALUE key, VALUE val, VALUE _arg) raise_generator_error(key, "%"PRIsVALUE" not allowed as object key in JSON", CLASS_OF(key)); } } + + json_inspect_hash_with_mixed_keys(arg); + key_to_s = rb_convert_type(key, T_STRING, "String", "to_s"); break; } diff --git a/lib/json/truffle_ruby/generator.rb b/lib/json/truffle_ruby/generator.rb index 1cacd76c5..f73708f2d 100644 --- a/lib/json/truffle_ruby/generator.rb +++ b/lib/json/truffle_ruby/generator.rb @@ -451,7 +451,11 @@ def generate(obj, anIO = nil) end obj.each_pair do |k,v| if first - key_type = k.class + if ::String == k.class || ::Symbol == k.class + key_type = k.class + else + JSON.send(:on_mixed_keys_hash, obj) + end else if key_type && !@allow_duplicate_key && key_type != k.class key_type = nil # stop checking @@ -592,7 +596,11 @@ def json_transform(state) hash.each { |key, value| if first - key_type = key.class + if ::String == key.class || ::Symbol == key.class + key_type = key.class + else + JSON.send(:on_mixed_keys_hash, hash) + end else if key_type && !state.allow_duplicate_key? && key_type != key.class key_type = nil # stop checking diff --git a/test/json/json_generator_test.rb b/test/json/json_generator_test.rb index 1d9318fd5..bfc324104 100755 --- a/test/json/json_generator_test.rb +++ b/test/json/json_generator_test.rb @@ -1202,11 +1202,32 @@ def test_generate_duplicate_keys_allowed end def test_generate_duplicate_keys_disallowed - hash = { foo: 1, "foo" => 2 } + assert_duplicate_key({ foo: 1, "foo" => 2 }) + assert_duplicate_key({ "1" => 1, 1 => 2 }) + assert_duplicate_key({ :"1" => 1, 1 => 2 }) + + fake_string = Class.new do + def initialize(str) + @str = str + end + + def to_s + @str.dup + end + end + + assert_duplicate_key({ fake_string.new("a") => 1, fake_string.new("a") => 2 }) + end + + def assert_duplicate_key(hash) error = assert_raise JSON::GeneratorError do JSON.generate(hash, allow_duplicate_key: false) end - assert_equal %(detected duplicate key "foo" in #{hash.inspect}), error.message + assert_equal %(detected duplicate key "#{hash.keys.first}" in #{hash.inspect}), error.message + end + + def test_generate_mixed_keys_without_duplicates + assert_equal %({"1":1,"2":2}), JSON.generate({ "1" => 1, 2 => 2 }, allow_duplicate_key: false) end def test_frozen