Skip to content

Commit 96397cf

Browse files
authored
Merge pull request #668 from casperisfine/non-string-keys
JSON.generate: call to_json on String subclasses
2 parents 6d3b3ac + 9d47305 commit 96397cf

File tree

5 files changed

+78
-9
lines changed

5 files changed

+78
-9
lines changed

ext/json/ext/fbuffer/fbuffer.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ static VALUE fbuffer_to_s(FBuffer *fb);
4242
#define RB_UNLIKELY(expr) expr
4343
#endif
4444

45+
#ifndef RB_LIKELY
46+
#define RB_LIKELY(expr) expr
47+
#endif
48+
4549
static void fbuffer_stack_init(FBuffer *fb, unsigned long initial_length, char *stack_buffer, long stack_buffer_size)
4650
{
4751
fb->initial_length = (initial_length > 0) ? initial_length : FBUFFER_INITIAL_LENGTH_DEFAULT;

ext/json/ext/generator/generator.c

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -737,7 +737,11 @@ json_object_i(VALUE key, VALUE val, VALUE _arg)
737737
break;
738738
}
739739

740-
generate_json_string(buffer, data, state, key_to_s);
740+
if (RB_LIKELY(RBASIC_CLASS(key_to_s) == rb_cString)) {
741+
generate_json_string(buffer, data, state, key_to_s);
742+
} else {
743+
generate_json(buffer, data, state, key_to_s);
744+
}
741745
if (RB_UNLIKELY(state->space_before)) fbuffer_append_str(buffer, state->space_before);
742746
fbuffer_append_char(buffer, ':');
743747
if (RB_UNLIKELY(state->space)) fbuffer_append_str(buffer, state->space);

java/src/json/ext/Generator.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,14 @@ public void visit(IRubyObject key, IRubyObject value) {
359359
}
360360
if (objectNl.length() != 0) buffer.append(indent);
361361

362-
STRING_HANDLER.generate(session, key.asString(), buffer);
362+
IRubyObject keyStr = key.callMethod(context, "to_s");
363+
if (keyStr.getMetaClass() == runtime.getString()) {
364+
STRING_HANDLER.generate(session, (RubyString)keyStr, buffer);
365+
} else {
366+
Utils.ensureString(keyStr);
367+
Handler<IRubyObject> keyHandler = (Handler<IRubyObject>) getHandlerFor(runtime, keyStr);
368+
keyHandler.generate(session, keyStr, buffer);
369+
}
363370
session.infectBy(key);
364371

365372
buffer.append(spaceBefore);

lib/json/pure/generator.rb

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -307,19 +307,30 @@ def generate(obj)
307307

308308
# Handles @allow_nan, @buffer_initial_length, other ivars must be the default value (see above)
309309
private def generate_json(obj, buf)
310-
case obj
311-
when Hash
310+
klass = obj.class
311+
if klass == Hash
312312
buf << '{'
313313
first = true
314314
obj.each_pair do |k,v|
315315
buf << ',' unless first
316-
fast_serialize_string(k.to_s, buf)
316+
317+
key_str = k.to_s
318+
if key_str.is_a?(::String)
319+
if key_str.class == ::String
320+
fast_serialize_string(key_str, buf)
321+
else
322+
generate_json(key_str, buf)
323+
end
324+
else
325+
raise TypeError, "#{k.class}#to_s returns an instance of #{key_str.class}, expected a String"
326+
end
327+
317328
buf << ':'
318329
generate_json(v, buf)
319330
first = false
320331
end
321332
buf << '}'
322-
when Array
333+
elsif klass == Array
323334
buf << '['
324335
first = true
325336
obj.each do |e|
@@ -328,9 +339,9 @@ def generate(obj)
328339
first = false
329340
end
330341
buf << ']'
331-
when String
342+
elsif klass == String
332343
fast_serialize_string(obj, buf)
333-
when Integer
344+
elsif klass == Integer
334345
buf << obj.to_s
335346
else
336347
# Note: Float is handled this way since Float#to_s is slow anyway
@@ -419,7 +430,15 @@ def json_transform(state)
419430
each { |key, value|
420431
result << delim unless first
421432
result << state.indent * depth if indent
422-
result = +"#{result}#{key.to_s.to_json(state)}#{state.space_before}:#{state.space}"
433+
434+
key_str = key.to_s
435+
key_json = if key_str.is_a?(::String)
436+
key_str = key_str.to_json(state)
437+
else
438+
raise TypeError, "#{key.class}#to_s returns an instance of #{key_str.class}, expected a String"
439+
end
440+
441+
result = +"#{result}#{key_json}#{state.space_before}:#{state.space}"
423442
if state.strict? && !(false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value)
424443
raise GeneratorError, "#{value.class} not allowed in JSON"
425444
elsif value.respond_to?(:to_json)

test/json/json_generator_test.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,41 @@ def test_invalid_encoding_string
486486
end
487487
end
488488

489+
class MyCustomString < String
490+
def to_json(_state = nil)
491+
'"my_custom_key"'
492+
end
493+
494+
def to_s
495+
self
496+
end
497+
end
498+
499+
def test_string_subclass_as_keys
500+
# Ref: https://github.com/ruby/json/issues/667
501+
# if key.to_s doesn't return a bare string, we call `to_json` on it.
502+
key = MyCustomString.new("won't be used")
503+
assert_equal '{"my_custom_key":1}', JSON.generate(key => 1)
504+
end
505+
506+
class FakeString
507+
def to_json(_state = nil)
508+
raise "Shouldn't be called"
509+
end
510+
511+
def to_s
512+
self
513+
end
514+
end
515+
516+
def test_custom_object_as_keys
517+
key = FakeString.new
518+
error = assert_raise(TypeError) do
519+
JSON.generate(key => 1)
520+
end
521+
assert_match "FakeString", error.message
522+
end
523+
489524
def test_to_json_called_with_state_object
490525
object = Object.new
491526
called = false

0 commit comments

Comments
 (0)