Skip to content

Commit 3a98832

Browse files
authored
Merge pull request #864 from byroot/as-json-key
`JSON::Coder` callback now recieve a second argument to mark object keys
2 parents 8fdc0ec + 4d9068c commit 3a98832

File tree

7 files changed

+49
-25
lines changed

7 files changed

+49
-25
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
### Unreleased
44

5+
* `JSON::Coder` callback now receive a second argument to convey whether the object is a hash key.
56
* Tuned the floating point number generator to not use scientific notation as agressively.
67

78
### 2025-09-18 (2.14.1)

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ Instead it is recommended to use the newer `JSON::Coder` API:
9797

9898
```ruby
9999
module MyApp
100-
API_JSON_CODER = JSON::Coder.new do |object|
100+
API_JSON_CODER = JSON::Coder.new do |object, is_object_key|
101101
case object
102102
when Time
103103
object.iso8601(3)
@@ -113,6 +113,8 @@ puts MyApp::API_JSON_CODER.dump(Time.now.utc) # => "2025-01-21T08:41:44.286Z"
113113
The provided block is called for all objects that don't have a native JSON equivalent, and
114114
must return a Ruby object that has a native JSON equivalent.
115115

116+
It is also called for objects that do have a JSON equivalent, but are used as Hash keys, for instance `{ 1 => 2}`.
117+
116118
## Combining JSON fragments
117119

118120
To combine JSON fragments into a bigger JSON document, you can use `JSON::Fragment`:

ext/json/ext/generator/generator.c

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ typedef struct JSON_Generator_StateStruct {
2929

3030
enum duplicate_key_action on_duplicate_key;
3131

32+
bool as_json_single_arg;
3233
bool allow_nan;
3334
bool ascii_only;
3435
bool script_safe;
@@ -1033,6 +1034,13 @@ json_inspect_hash_with_mixed_keys(struct hash_foreach_arg *arg)
10331034
}
10341035
}
10351036

1037+
static VALUE
1038+
json_call_as_json(JSON_Generator_State *state, VALUE object, VALUE is_key)
1039+
{
1040+
VALUE proc_args[2] = {object, is_key};
1041+
return rb_proc_call_with_block(state->as_json, 2, proc_args, Qnil);
1042+
}
1043+
10361044
static int
10371045
json_object_i(VALUE key, VALUE val, VALUE _arg)
10381046
{
@@ -1086,7 +1094,7 @@ json_object_i(VALUE key, VALUE val, VALUE _arg)
10861094
default:
10871095
if (data->state->strict) {
10881096
if (RTEST(data->state->as_json) && !as_json_called) {
1089-
key = rb_proc_call_with_block(data->state->as_json, 1, &key, Qnil);
1097+
key = json_call_as_json(data->state, key, Qtrue);
10901098
key_type = rb_type(key);
10911099
as_json_called = true;
10921100
goto start;
@@ -1328,7 +1336,7 @@ static void generate_json_float(FBuffer *buffer, struct generate_json_data *data
13281336
/* for NaN and Infinity values we either raise an error or rely on Float#to_s. */
13291337
if (!allow_nan) {
13301338
if (data->state->strict && data->state->as_json) {
1331-
VALUE casted_obj = rb_proc_call_with_block(data->state->as_json, 1, &obj, Qnil);
1339+
VALUE casted_obj = json_call_as_json(data->state, obj, Qfalse);
13321340
if (casted_obj != obj) {
13331341
increase_depth(data);
13341342
generate_json(buffer, data, casted_obj);
@@ -1416,7 +1424,7 @@ static void generate_json(FBuffer *buffer, struct generate_json_data *data, VALU
14161424
general:
14171425
if (data->state->strict) {
14181426
if (RTEST(data->state->as_json) && !as_json_called) {
1419-
obj = rb_proc_call_with_block(data->state->as_json, 1, &obj, Qnil);
1427+
obj = json_call_as_json(data->state, obj, Qfalse);
14201428
as_json_called = true;
14211429
goto start;
14221430
} else {
@@ -1942,6 +1950,7 @@ static int configure_state_i(VALUE key, VALUE val, VALUE _arg)
19421950
else if (key == sym_allow_duplicate_key) { state->on_duplicate_key = RTEST(val) ? JSON_IGNORE : JSON_RAISE; }
19431951
else if (key == sym_as_json) {
19441952
VALUE proc = RTEST(val) ? rb_convert_type(val, T_DATA, "Proc", "to_proc") : Qfalse;
1953+
state->as_json_single_arg = proc && rb_proc_arity(proc) == 1;
19451954
state_write_value(data, &state->as_json, proc);
19461955
}
19471956
return ST_CONTINUE;

java/src/json/ext/Generator.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ static void generateFloat(ThreadContext context, Session session, RubyFloat obje
396396

397397
if (!state.allowNaN()) {
398398
if (state.strict() && state.getAsJSON() != null) {
399-
IRubyObject castedValue = state.getAsJSON().call(context, object);
399+
IRubyObject castedValue = state.getAsJSON().call(context, object, context.getRuntime().getFalse());
400400
if (castedValue != object) {
401401
getHandlerFor(context.runtime, castedValue).generate(context, session, castedValue, buffer);
402402
return;
@@ -623,7 +623,7 @@ private static void processEntry(ThreadContext context, Session session, OutputS
623623
GeneratorState state = session.getState(context);
624624
if (state.strict()) {
625625
if (state.getAsJSON() != null) {
626-
key = state.getAsJSON().call(context, key);
626+
key = state.getAsJSON().call(context, key, context.getRuntime().getTrue());
627627
keyStr = castKey(context, key);
628628
}
629629

@@ -760,7 +760,7 @@ static RubyString generateGenericNew(ThreadContext context, Session session, IRu
760760
GeneratorState state = session.getState(context);
761761
if (state.strict()) {
762762
if (state.getAsJSON() != null) {
763-
IRubyObject value = state.getAsJSON().call(context, object);
763+
IRubyObject value = state.getAsJSON().call(context, object, context.getRuntime().getFalse());
764764
Handler handler = getHandlerFor(context.runtime, value);
765765
if (handler == GENERIC_HANDLER) {
766766
throw Utils.buildGeneratorError(context, object, value + " returned by as_json not allowed in JSON").toThrowable();

lib/json/truffle_ruby/generator.rb

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ module Generator
4747

4848
SCRIPT_SAFE_ESCAPE_PATTERN = /[\/"\\\x0-\x1f\u2028-\u2029]/
4949

50+
def self.native_type?(value) # :nodoc:
51+
(false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value || Fragment === value)
52+
end
53+
54+
def self.native_key?(key) # :nodoc:
55+
(Symbol === key || String === key)
56+
end
57+
5058
# Convert a UTF8 encoded Ruby string _string_ to a JSON string, encoded with
5159
# UTF16 big endian characters as \u????, and return it.
5260
def self.utf8_to_json(string, script_safe = false) # :nodoc:
@@ -448,10 +456,10 @@ def to_json(state = nil, *)
448456
state = State.from_state(state) if state
449457
if state&.strict?
450458
value = self
451-
if state.strict? && !(false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value || Fragment === value)
459+
if state.strict? && !Generator.native_type?(value)
452460
if state.as_json
453-
value = state.as_json.call(value)
454-
unless false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value || Fragment === value
461+
value = state.as_json.call(value, false)
462+
unless Generator.native_type?(value)
455463
raise GeneratorError.new("#{value.class} returned by #{state.as_json} not allowed in JSON", value)
456464
end
457465
value.to_json(state)
@@ -509,12 +517,12 @@ def json_transform(state)
509517
end
510518
result << state.indent * depth if indent
511519

512-
if state.strict? && !(Symbol === key || String === key)
520+
if state.strict? && !Generator.native_key?(key)
513521
if state.as_json
514-
key = state.as_json.call(key)
522+
key = state.as_json.call(key, true)
515523
end
516524

517-
unless Symbol === key || String === key
525+
unless Generator.native_key?(key)
518526
raise GeneratorError.new("#{key.class} not allowed as object key in JSON", value)
519527
end
520528
end
@@ -527,10 +535,10 @@ def json_transform(state)
527535
end
528536

529537
result = +"#{result}#{key_json}#{state.space_before}:#{state.space}"
530-
if state.strict? && !(false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value || Fragment === value)
538+
if state.strict? && !Generator.native_type?(value)
531539
if state.as_json
532-
value = state.as_json.call(value)
533-
unless false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value || Fragment === value
540+
value = state.as_json.call(value, false)
541+
unless Generator.native_type?(value)
534542
raise GeneratorError.new("#{value.class} returned by #{state.as_json} not allowed in JSON", value)
535543
end
536544
result << value.to_json(state)
@@ -588,10 +596,10 @@ def json_transform(state)
588596
each { |value|
589597
result << delim unless first
590598
result << state.indent * depth if indent
591-
if state.strict? && !(false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value || Fragment === value || Symbol == value)
599+
if state.strict? && !Generator.native_type?(value)
592600
if state.as_json
593-
value = state.as_json.call(value)
594-
unless false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value || Fragment === value || Symbol === value
601+
value = state.as_json.call(value, false)
602+
unless Generator.native_type?(value)
595603
raise GeneratorError.new("#{value.class} returned by #{state.as_json} not allowed in JSON", value)
596604
end
597605
result << value.to_json(state)
@@ -625,7 +633,7 @@ def to_json(state = nil, *args)
625633
if state.allow_nan?
626634
to_s
627635
elsif state.strict? && state.as_json
628-
casted_value = state.as_json.call(self)
636+
casted_value = state.as_json.call(self, false)
629637

630638
if casted_value.equal?(self)
631639
raise GeneratorError.new("#{self} not allowed in JSON", self)

test/json/json_coder_test.rb

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,19 @@ def test_json_coder_with_proc
1212
end
1313

1414
def test_json_coder_with_proc_with_unsupported_value
15-
coder = JSON::Coder.new do |object|
15+
coder = JSON::Coder.new do |object, is_key|
16+
assert_equal false, is_key
1617
Object.new
1718
end
1819
assert_raise(JSON::GeneratorError) { coder.dump([Object.new]) }
1920
end
2021

2122
def test_json_coder_hash_key
2223
obj = Object.new
23-
coder = JSON::Coder.new(&:to_s)
24+
coder = JSON::Coder.new do |obj, is_key|
25+
assert_equal true, is_key
26+
obj.to_s
27+
end
2428
assert_equal %({#{obj.to_s.inspect}:1}), coder.dump({ obj => 1 })
2529

2630
coder = JSON::Coder.new { 42 }
@@ -49,14 +53,14 @@ def test_json_coder_load_options
4953
end
5054

5155
def test_json_coder_dump_NaN_or_Infinity
52-
coder = JSON::Coder.new(&:inspect)
56+
coder = JSON::Coder.new { |o| o.inspect }
5357
assert_equal "NaN", coder.load(coder.dump(Float::NAN))
5458
assert_equal "Infinity", coder.load(coder.dump(Float::INFINITY))
5559
assert_equal "-Infinity", coder.load(coder.dump(-Float::INFINITY))
5660
end
5761

5862
def test_json_coder_dump_NaN_or_Infinity_loop
59-
coder = JSON::Coder.new(&:itself)
63+
coder = JSON::Coder.new { |o| o.itself }
6064
error = assert_raise JSON::GeneratorError do
6165
coder.dump(Float::NAN)
6266
end

test/json/json_generator_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -822,7 +822,7 @@ def test_fragment
822822

823823
def test_json_generate_as_json_convert_to_proc
824824
object = Object.new
825-
assert_equal object.object_id.to_json, JSON.generate(object, strict: true, as_json: :object_id)
825+
assert_equal object.object_id.to_json, JSON.generate(object, strict: true, as_json: -> (o, is_key) { o.object_id })
826826
end
827827

828828
def assert_float_roundtrip(expected, actual)

0 commit comments

Comments
 (0)