diff --git a/CHANGES.md b/CHANGES.md index ea59edf7..fc055710 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,7 @@ # Changes +* Handle non-string hash keys with broken `to_s` implementations. + ### 2025-04-25 (2.11.3) * Fix a regression in `JSON.pretty_generate` that could cause indentation to be off once some `#to_json` has been called. diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c index 981bf96b..ba105664 100644 --- a/ext/json/ext/generator/generator.c +++ b/ext/json/ext/generator/generator.c @@ -789,6 +789,21 @@ struct hash_foreach_arg { int iter; }; +static VALUE +convert_string_subclass(VALUE key) +{ + VALUE key_to_s = rb_funcall(key, i_to_s, 0); + + if (RB_UNLIKELY(rb_type(key_to_s) != T_STRING)) { + VALUE cname = rb_obj_class(key); + rb_raise(rb_eTypeError, + "can't convert %"PRIsVALUE" to %s (%"PRIsVALUE"#%s gives %"PRIsVALUE")", + cname, "String", cname, "to_s", rb_obj_class(key_to_s)); + } + + return key_to_s; +} + static int json_object_i(VALUE key, VALUE val, VALUE _arg) { @@ -817,7 +832,7 @@ json_object_i(VALUE key, VALUE val, VALUE _arg) if (RB_LIKELY(RBASIC_CLASS(key) == rb_cString)) { key_to_s = key; } else { - key_to_s = rb_funcall(key, i_to_s, 0); + key_to_s = convert_string_subclass(key); } break; case T_SYMBOL: diff --git a/test/json/json_generator_test.rb b/test/json/json_generator_test.rb index f87006ac..47c94bd3 100755 --- a/test/json/json_generator_test.rb +++ b/test/json/json_generator_test.rb @@ -605,6 +605,22 @@ def test_string_subclass_with_to_s assert_equal '{"JSONGeneratorTest::StringWithToS#to_s":1}', JSON.generate(StringWithToS.new => 1) end + def test_string_subclass_with_broken_to_s + klass = Class.new(String) do + def to_s + false + end + end + s = klass.new("test") + assert_equal '["test"]', JSON.generate([s]) + + omit("Can't figure out how to match behavior in java code") if RUBY_PLATFORM == "java" + + assert_raise TypeError do + JSON.generate(s => 1) + end + end + if defined?(JSON::Ext::Generator) and RUBY_PLATFORM != "java" def test_valid_utf8_in_different_encoding utf8_string = "€™"