Skip to content

Commit 12dc394

Browse files
committed
Handle non-string keys returning immediate values via to_s
We can't directly call `RBASIC_CLASS` as the return value of `to_s` may be an immediate.
1 parent 3e025f7 commit 12dc394

File tree

3 files changed

+34
-1
lines changed

3 files changed

+34
-1
lines changed

CHANGES.md

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Changes
22

3+
* Handle non-string hash keys with broken `to_s` implementations.
4+
35
### 2025-04-25 (2.11.3)
46

57
* Fix a regression in `JSON.pretty_generate` that could cause indentation to be off once some `#to_json` has been called.

ext/json/ext/generator/generator.c

+16-1
Original file line numberDiff line numberDiff line change
@@ -789,6 +789,21 @@ struct hash_foreach_arg {
789789
int iter;
790790
};
791791

792+
static VALUE
793+
convert_string_subclass(VALUE key)
794+
{
795+
VALUE key_to_s = rb_funcall(key, i_to_s, 0);
796+
797+
if (RB_UNLIKELY(rb_type(key_to_s) != T_STRING)) {
798+
VALUE cname = rb_obj_class(key);
799+
rb_raise(rb_eTypeError,
800+
"can't convert %"PRIsVALUE" to %s (%"PRIsVALUE"#%s gives %"PRIsVALUE")",
801+
cname, "String", cname, "to_s", rb_obj_class(key_to_s));
802+
}
803+
804+
return key_to_s;
805+
}
806+
792807
static int
793808
json_object_i(VALUE key, VALUE val, VALUE _arg)
794809
{
@@ -817,7 +832,7 @@ json_object_i(VALUE key, VALUE val, VALUE _arg)
817832
if (RB_LIKELY(RBASIC_CLASS(key) == rb_cString)) {
818833
key_to_s = key;
819834
} else {
820-
key_to_s = rb_funcall(key, i_to_s, 0);
835+
key_to_s = convert_string_subclass(key);
821836
}
822837
break;
823838
case T_SYMBOL:

test/json/json_generator_test.rb

+16
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,22 @@ def test_string_subclass_with_to_s
605605
assert_equal '{"JSONGeneratorTest::StringWithToS#to_s":1}', JSON.generate(StringWithToS.new => 1)
606606
end
607607

608+
def test_string_subclass_with_broken_to_s
609+
klass = Class.new(String) do
610+
def to_s
611+
false
612+
end
613+
end
614+
s = klass.new("test")
615+
assert_equal '["test"]', JSON.generate([s])
616+
617+
omit("Can't figure out how to match behavior in java code") if RUBY_PLATFORM == "java"
618+
619+
assert_raise TypeError do
620+
JSON.generate(s => 1)
621+
end
622+
end
623+
608624
if defined?(JSON::Ext::Generator) and RUBY_PLATFORM != "java"
609625
def test_valid_utf8_in_different_encoding
610626
utf8_string = "€™"

0 commit comments

Comments
 (0)