Skip to content

Commit 1b47fe8

Browse files
committed
Fix JSON::Coder to cast non-string keys.
1 parent 6b48c01 commit 1b47fe8

4 files changed

Lines changed: 61 additions & 16 deletions

File tree

ext/json/ext/generator/generator.c

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1029,6 +1029,9 @@ json_object_i(VALUE key, VALUE val, VALUE _arg)
10291029
}
10301030

10311031
VALUE key_to_s;
1032+
bool as_json_called = false;
1033+
1034+
start:
10321035
switch (rb_type(key)) {
10331036
case T_STRING:
10341037
if (RB_LIKELY(RBASIC_CLASS(key) == rb_cString)) {
@@ -1042,7 +1045,13 @@ json_object_i(VALUE key, VALUE val, VALUE _arg)
10421045
break;
10431046
default:
10441047
if (data->state->strict) {
1045-
raise_generator_error(key, "%"PRIsVALUE" not allowed in JSON", rb_funcall(key, i_to_s, 0));
1048+
if (RTEST(data->state->as_json) && !as_json_called) {
1049+
key = rb_proc_call_with_block(data->state->as_json, 1, &key, Qnil);
1050+
as_json_called = true;
1051+
goto start;
1052+
} else {
1053+
raise_generator_error(key, "%"PRIsVALUE" not allowed as object key in JSON", CLASS_OF(key));
1054+
}
10461055
}
10471056
key_to_s = rb_convert_type(key, T_STRING, "String", "to_s");
10481057
break;

java/src/json/ext/Generator.java

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,23 @@ static void generateHash(ThreadContext context, Session session, RubyHash object
520520
buffer.write('}');
521521
}
522522

523+
private static IRubyObject castKey(ThreadContext context, IRubyObject key) {
524+
RubyClass keyClass = key.getType();
525+
Ruby runtime = context.runtime;
526+
527+
if (key instanceof RubyString) {
528+
if (keyClass == runtime.getString()) {
529+
return key;
530+
} else {
531+
return key.callMethod(context, "to_s");
532+
}
533+
} else if (keyClass == runtime.getSymbol()) {
534+
return ((RubySymbol) key).id2name(context);
535+
} else {
536+
return null;
537+
}
538+
}
539+
523540
private static void processEntry(ThreadContext context, Session session, OutputStream buffer, RubyHash.RubyHashEntry entry, boolean firstPair, ByteList objectNl, byte[] indent, ByteList spaceBefore, ByteList space) {
524541
IRubyObject key = (IRubyObject) entry.getKey();
525542
IRubyObject value = (IRubyObject) entry.getValue();
@@ -533,21 +550,22 @@ private static void processEntry(ThreadContext context, Session session, OutputS
533550

534551
Ruby runtime = context.runtime;
535552

536-
IRubyObject keyStr;
537-
RubyClass keyClass = key.getType();
538-
if (key instanceof RubyString) {
539-
if (keyClass == runtime.getString()) {
540-
keyStr = key;
541-
} else {
542-
keyStr = key.callMethod(context, "to_s");
553+
IRubyObject keyStr = castKey(context, key);
554+
if (keyStr == null || !(keyStr instanceof RubyString)) {
555+
GeneratorState state = session.getState(context);
556+
if (state.strict()) {
557+
if (state.getAsJSON() != null) {
558+
key = state.getAsJSON().call(context, key);
559+
keyStr = castKey(context, key);
560+
}
561+
562+
if (keyStr == null) {
563+
throw Utils.buildGeneratorError(context, key, key.getType().name(context) + " not allowed as object key in JSON").toThrowable();
564+
}
543565
}
544-
} else if (keyClass == runtime.getSymbol()) {
545-
keyStr = ((RubySymbol) key).id2name(context);
546-
} else {
547-
if (session.getState(context).strict()) {
548-
throw Utils.buildGeneratorError(context, key, key + " not allowed in JSON").toThrowable();
566+
else {
567+
keyStr = TypeConverter.convertToType(key, runtime.getString(), "to_s");
549568
}
550-
keyStr = TypeConverter.convertToType(key, runtime.getString(), "to_s");
551569
}
552570

553571
if (keyStr.getMetaClass() == runtime.getString()) {
@@ -673,7 +691,7 @@ void generate(ThreadContext context, Session session, IRubyObject object, Output
673691
static RubyString generateGenericNew(ThreadContext context, Session session, IRubyObject object) {
674692
GeneratorState state = session.getState(context);
675693
if (state.strict()) {
676-
if (state.getAsJSON() != null ) {
694+
if (state.getAsJSON() != null) {
677695
IRubyObject value = state.getAsJSON().call(context, object);
678696
Handler handler = getHandlerFor(context.runtime, value);
679697
if (handler == GENERIC_HANDLER) {

lib/json/truffle_ruby/generator.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,13 @@ def json_transform(state)
477477
result << state.indent * depth if indent
478478

479479
if state.strict? && !(Symbol === key || String === key)
480-
raise GeneratorError.new("#{key.class} not allowed in JSON", value)
480+
if state.as_json
481+
key = state.as_json.call(key)
482+
end
483+
484+
unless Symbol === key || String === key
485+
raise GeneratorError.new("#{key.class} not allowed as object key in JSON", value)
486+
end
481487
end
482488

483489
key_str = key.to_s

test/json/json_coder_test.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@ def test_json_coder_with_proc_with_unsupported_value
1818
assert_raise(JSON::GeneratorError) { coder.dump([Object.new]) }
1919
end
2020

21+
def test_json_coder_hash_key
22+
obj = Object.new
23+
coder = JSON::Coder.new(&:to_s)
24+
assert_equal %({#{obj.to_s.inspect}:1}), coder.dump({ obj => 1 })
25+
26+
coder = JSON::Coder.new { 42 }
27+
error = assert_raise JSON::GeneratorError do
28+
coder.dump({ obj => 1 })
29+
end
30+
assert_equal "Integer not allowed as object key in JSON", error.message
31+
end
32+
2133
def test_json_coder_options
2234
coder = JSON::Coder.new(array_nl: "\n") do |object|
2335
42

0 commit comments

Comments
 (0)