From 1566cd01a60bf290d75b00b9b4308f396ba3827a Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Thu, 18 Sep 2025 18:57:26 +0200 Subject: [PATCH 1/9] Avoid scientific notation before exponent 15 Fix: https://github.com/ruby/json/issues/861 It's not incorrect to use scientific notation, but it tend to throw people off a bit, so it's best to keep it for very large numbers. --- CHANGES.md | 2 ++ ext/json/ext/generator/generator.c | 5 ++- ext/json/ext/vendor/fpconv.c | 23 ++++++------- test/json/json_generator_test.rb | 53 +++++++++++++++++++----------- 4 files changed, 50 insertions(+), 33 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 5f50531b3..7120367f0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,8 @@ ### Unreleased +* Tuned the floating point number generator to not use scientific notation as agressively. + ### 2025-09-18 (2.14.1) * Fix `IndexOutOfBoundsException` in the JRuby extension when encoding shared strings. diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c index 8fcc980d4..9b67629f9 100644 --- a/ext/json/ext/generator/generator.c +++ b/ext/json/ext/generator/generator.c @@ -1345,12 +1345,11 @@ static void generate_json_float(FBuffer *buffer, struct generate_json_data *data } /* This implementation writes directly into the buffer. We reserve - * the 28 characters that fpconv_dtoa states as its maximum. + * the 32 characters that fpconv_dtoa states as its maximum. */ - fbuffer_inc_capa(buffer, 28); + fbuffer_inc_capa(buffer, 32); char* d = buffer->ptr + buffer->len; int len = fpconv_dtoa(value, d); - /* fpconv_dtoa converts a float to its shortest string representation, * but it adds a ".0" if this is a plain integer. */ diff --git a/ext/json/ext/vendor/fpconv.c b/ext/json/ext/vendor/fpconv.c index 75efd46f1..e91c7889c 100644 --- a/ext/json/ext/vendor/fpconv.c +++ b/ext/json/ext/vendor/fpconv.c @@ -29,6 +29,10 @@ #include #include +#ifdef JSON_DEBUG +#include +#endif + #define npowers 87 #define steppowers 8 #define firstpower -348 /* 10 ^ -348 */ @@ -320,15 +324,7 @@ static int emit_digits(char* digits, int ndigits, char* dest, int K, bool neg) { int exp = absv(K + ndigits - 1); - int max_trailing_zeros = 7; - - if(neg) { - max_trailing_zeros -= 1; - } - - /* write plain integer */ - if(K >= 0 && (exp < (ndigits + max_trailing_zeros))) { - + if(K >= 0 && exp < 15) { memcpy(dest, digits, ndigits); memset(dest + ndigits, '0', K); @@ -432,10 +428,12 @@ static int filter_special(double fp, char* dest) * * Input: * fp -> the double to convert, dest -> destination buffer. - * The generated string will never be longer than 28 characters. - * Make sure to pass a pointer to at least 28 bytes of memory. + * The generated string will never be longer than 32 characters. + * Make sure to pass a pointer to at least 32 bytes of memory. * The emitted string will not be null terminated. * + * + * * Output: * The number of written characters. * @@ -474,6 +472,9 @@ static int fpconv_dtoa(double d, char dest[28]) int ndigits = grisu2(d, digits, &K); str_len += emit_digits(digits, ndigits, dest + str_len, K, neg); +#ifdef JSON_DEBUG + assert(str_len <= 32); +#endif return str_len; } diff --git a/test/json/json_generator_test.rb b/test/json/json_generator_test.rb index 6b42de2ad..4fdfa12b0 100755 --- a/test/json/json_generator_test.rb +++ b/test/json/json_generator_test.rb @@ -825,26 +825,41 @@ def test_json_generate_as_json_convert_to_proc assert_equal object.object_id.to_json, JSON.generate(object, strict: true, as_json: :object_id) end - def test_json_generate_float - values = [-1.0, 1.0, 0.0, 12.2, 7.5 / 3.2, 12.0, 100.0, 1000.0] - expecteds = ["-1.0", "1.0", "0.0", "12.2", "2.34375", "12.0", "100.0", "1000.0"] - - if RUBY_ENGINE == "jruby" - values << 1746861937.7842371 - expecteds << "1.7468619377842371E9" - else - values << 1746861937.7842371 - expecteds << "1746861937.7842371" - end - - if RUBY_ENGINE == "ruby" - values << -2.2471348024634545e-08 << -2.2471348024634545e-09 << -2.2471348024634545e-10 - expecteds << "-0.000000022471348024634545" << "-0.0000000022471348024634545" << "-2.2471348024634546e-10" - end + def assert_float_roundtrip(expected, actual) + assert_equal(expected, JSON.generate(actual)) + assert_equal(actual, JSON.parse(JSON.generate(actual)), "JSON: #{JSON.generate(actual)}") + end - values.zip(expecteds).each do |value, expected| - assert_equal expected, value.to_json - end + def test_json_generate_float + assert_float_roundtrip "-1.0", -1.0 + assert_float_roundtrip "1.0", 1.0 + assert_float_roundtrip "0.0", 0.0 + assert_float_roundtrip "12.2", 12.2 + assert_float_roundtrip "2.34375", 7.5 / 3.2 + assert_float_roundtrip "12.0", 12.0 + assert_float_roundtrip "100.0", 100.0 + assert_float_roundtrip "1000.0", 1000.0 + + if RUBY_ENGINE == "jruby" + assert_float_roundtrip "1.7468619377842371E9", 1746861937.7842371 + else + assert_float_roundtrip "1746861937.7842371", 1746861937.7842371 + end + + if RUBY_ENGINE == "ruby" + assert_float_roundtrip "100000000000000.0", 100000000000000.0 + assert_float_roundtrip "1e+15", 1e+15 + assert_float_roundtrip "-100000000000000.0", -100000000000000.0 + assert_float_roundtrip "-1e+15", -1e+15 + assert_float_roundtrip "1111111111111111.1", 1111111111111111.1 + assert_float_roundtrip "1.1111111111111112e+16", 11111111111111111.1 + assert_float_roundtrip "-1111111111111111.1", -1111111111111111.1 + assert_float_roundtrip "-1.1111111111111112e+16", -11111111111111111.1 + + assert_float_roundtrip "-0.000000022471348024634545", -2.2471348024634545e-08 + assert_float_roundtrip "-0.0000000022471348024634545", -2.2471348024634545e-09 + assert_float_roundtrip "-2.2471348024634546e-10", -2.2471348024634545e-10 + end end def test_numbers_of_various_sizes From 7982b6148cba71d50ab9aa26d84681c0e19ef135 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Thu, 18 Sep 2025 20:33:41 +0200 Subject: [PATCH 2/9] `JSON::Coder` callback now recieve a second argument to mark object keys e.g. ```ruby { 1 => 2 } ``` The callback will be invoked for `1` as while it has a native JSON equivalent, it's not legal as an object name. --- CHANGES.md | 1 + README.md | 4 +++- ext/json/ext/generator/generator.c | 15 ++++++++++++--- java/src/json/ext/Generator.java | 6 +++--- lib/json/truffle_ruby/generator.rb | 10 +++++----- test/json/json_coder_test.rb | 12 ++++++++---- test/json/json_generator_test.rb | 2 +- 7 files changed, 33 insertions(+), 17 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7120367f0..33929954f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ ### Unreleased +* `JSON::Coder` callback now receive a second argument to convey whether the object is a hash key. * Tuned the floating point number generator to not use scientific notation as agressively. ### 2025-09-18 (2.14.1) diff --git a/README.md b/README.md index 119327213..eed71ba43 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ Instead it is recommended to use the newer `JSON::Coder` API: ```ruby module MyApp - API_JSON_CODER = JSON::Coder.new do |object| + API_JSON_CODER = JSON::Coder.new do |object, is_object_key| case object when Time object.iso8601(3) @@ -113,6 +113,8 @@ puts MyApp::API_JSON_CODER.dump(Time.now.utc) # => "2025-01-21T08:41:44.286Z" The provided block is called for all objects that don't have a native JSON equivalent, and must return a Ruby object that has a native JSON equivalent. +It is also called for objects that do have a JSON equivalent, but are used as Hash keys, for instance `{ 1 => 2}`. + ## Combining JSON fragments To combine JSON fragments into a bigger JSON document, you can use `JSON::Fragment`: diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c index 9b67629f9..6a38cc60a 100644 --- a/ext/json/ext/generator/generator.c +++ b/ext/json/ext/generator/generator.c @@ -29,6 +29,7 @@ typedef struct JSON_Generator_StateStruct { enum duplicate_key_action on_duplicate_key; + bool as_json_single_arg; bool allow_nan; bool ascii_only; bool script_safe; @@ -1033,6 +1034,13 @@ json_inspect_hash_with_mixed_keys(struct hash_foreach_arg *arg) } } +static VALUE +json_call_as_json(JSON_Generator_State *state, VALUE object, VALUE is_key) +{ + VALUE proc_args[2] = {object, is_key}; + return rb_proc_call_with_block(state->as_json, 2, proc_args, Qnil); +} + static int json_object_i(VALUE key, VALUE val, VALUE _arg) { @@ -1086,7 +1094,7 @@ json_object_i(VALUE key, VALUE val, VALUE _arg) default: if (data->state->strict) { if (RTEST(data->state->as_json) && !as_json_called) { - key = rb_proc_call_with_block(data->state->as_json, 1, &key, Qnil); + key = json_call_as_json(data->state, key, Qtrue); key_type = rb_type(key); as_json_called = true; goto start; @@ -1328,7 +1336,7 @@ static void generate_json_float(FBuffer *buffer, struct generate_json_data *data /* for NaN and Infinity values we either raise an error or rely on Float#to_s. */ if (!allow_nan) { if (data->state->strict && data->state->as_json) { - VALUE casted_obj = rb_proc_call_with_block(data->state->as_json, 1, &obj, Qnil); + VALUE casted_obj = json_call_as_json(data->state, obj, Qfalse); if (casted_obj != obj) { increase_depth(data); generate_json(buffer, data, casted_obj); @@ -1416,7 +1424,7 @@ static void generate_json(FBuffer *buffer, struct generate_json_data *data, VALU general: if (data->state->strict) { if (RTEST(data->state->as_json) && !as_json_called) { - obj = rb_proc_call_with_block(data->state->as_json, 1, &obj, Qnil); + obj = json_call_as_json(data->state, obj, Qfalse); as_json_called = true; goto start; } else { @@ -1942,6 +1950,7 @@ static int configure_state_i(VALUE key, VALUE val, VALUE _arg) else if (key == sym_allow_duplicate_key) { state->on_duplicate_key = RTEST(val) ? JSON_IGNORE : JSON_RAISE; } else if (key == sym_as_json) { VALUE proc = RTEST(val) ? rb_convert_type(val, T_DATA, "Proc", "to_proc") : Qfalse; + state->as_json_single_arg = proc && rb_proc_arity(proc) == 1; state_write_value(data, &state->as_json, proc); } return ST_CONTINUE; diff --git a/java/src/json/ext/Generator.java b/java/src/json/ext/Generator.java index c8452ade6..be0e82579 100644 --- a/java/src/json/ext/Generator.java +++ b/java/src/json/ext/Generator.java @@ -396,7 +396,7 @@ static void generateFloat(ThreadContext context, Session session, RubyFloat obje if (!state.allowNaN()) { if (state.strict() && state.getAsJSON() != null) { - IRubyObject castedValue = state.getAsJSON().call(context, object); + IRubyObject castedValue = state.getAsJSON().call(context, object, context.getRuntime().getFalse()); if (castedValue != object) { getHandlerFor(context.runtime, castedValue).generate(context, session, castedValue, buffer); return; @@ -623,7 +623,7 @@ private static void processEntry(ThreadContext context, Session session, OutputS GeneratorState state = session.getState(context); if (state.strict()) { if (state.getAsJSON() != null) { - key = state.getAsJSON().call(context, key); + key = state.getAsJSON().call(context, key, context.getRuntime().getTrue()); keyStr = castKey(context, key); } @@ -760,7 +760,7 @@ static RubyString generateGenericNew(ThreadContext context, Session session, IRu GeneratorState state = session.getState(context); if (state.strict()) { if (state.getAsJSON() != null) { - IRubyObject value = state.getAsJSON().call(context, object); + IRubyObject value = state.getAsJSON().call(context, object, context.getRuntime().getFalse()); Handler handler = getHandlerFor(context.runtime, value); if (handler == GENERIC_HANDLER) { throw Utils.buildGeneratorError(context, object, value + " returned by as_json not allowed in JSON").toThrowable(); diff --git a/lib/json/truffle_ruby/generator.rb b/lib/json/truffle_ruby/generator.rb index 8be312f7a..0e935d50a 100644 --- a/lib/json/truffle_ruby/generator.rb +++ b/lib/json/truffle_ruby/generator.rb @@ -450,7 +450,7 @@ def to_json(state = nil, *) value = self if state.strict? && !(false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value || Fragment === value) if state.as_json - value = state.as_json.call(value) + value = state.as_json.call(value, false) unless false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value || Fragment === value raise GeneratorError.new("#{value.class} returned by #{state.as_json} not allowed in JSON", value) end @@ -511,7 +511,7 @@ def json_transform(state) if state.strict? && !(Symbol === key || String === key) if state.as_json - key = state.as_json.call(key) + key = state.as_json.call(key, true) end unless Symbol === key || String === key @@ -529,7 +529,7 @@ def json_transform(state) result = +"#{result}#{key_json}#{state.space_before}:#{state.space}" if state.strict? && !(false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value || Fragment === value) if state.as_json - value = state.as_json.call(value) + value = state.as_json.call(value, false) unless false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value || Fragment === value raise GeneratorError.new("#{value.class} returned by #{state.as_json} not allowed in JSON", value) end @@ -590,7 +590,7 @@ def json_transform(state) result << state.indent * depth if indent if state.strict? && !(false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value || Fragment === value || Symbol == value) if state.as_json - value = state.as_json.call(value) + value = state.as_json.call(value, false) unless false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value || Fragment === value || Symbol === value raise GeneratorError.new("#{value.class} returned by #{state.as_json} not allowed in JSON", value) end @@ -625,7 +625,7 @@ def to_json(state = nil, *args) if state.allow_nan? to_s elsif state.strict? && state.as_json - casted_value = state.as_json.call(self) + casted_value = state.as_json.call(self, false) if casted_value.equal?(self) raise GeneratorError.new("#{self} not allowed in JSON", self) diff --git a/test/json/json_coder_test.rb b/test/json/json_coder_test.rb index fc4aba296..c72483537 100755 --- a/test/json/json_coder_test.rb +++ b/test/json/json_coder_test.rb @@ -12,7 +12,8 @@ def test_json_coder_with_proc end def test_json_coder_with_proc_with_unsupported_value - coder = JSON::Coder.new do |object| + coder = JSON::Coder.new do |object, is_key| + assert_equal false, is_key Object.new end assert_raise(JSON::GeneratorError) { coder.dump([Object.new]) } @@ -20,7 +21,10 @@ def test_json_coder_with_proc_with_unsupported_value def test_json_coder_hash_key obj = Object.new - coder = JSON::Coder.new(&:to_s) + coder = JSON::Coder.new do |obj, is_key| + assert_equal true, is_key + obj.to_s + end assert_equal %({#{obj.to_s.inspect}:1}), coder.dump({ obj => 1 }) coder = JSON::Coder.new { 42 } @@ -49,14 +53,14 @@ def test_json_coder_load_options end def test_json_coder_dump_NaN_or_Infinity - coder = JSON::Coder.new(&:inspect) + coder = JSON::Coder.new { |o| o.inspect } assert_equal "NaN", coder.load(coder.dump(Float::NAN)) assert_equal "Infinity", coder.load(coder.dump(Float::INFINITY)) assert_equal "-Infinity", coder.load(coder.dump(-Float::INFINITY)) end def test_json_coder_dump_NaN_or_Infinity_loop - coder = JSON::Coder.new(&:itself) + coder = JSON::Coder.new { |o| o.itself } error = assert_raise JSON::GeneratorError do coder.dump(Float::NAN) end diff --git a/test/json/json_generator_test.rb b/test/json/json_generator_test.rb index 4fdfa12b0..a6950f888 100755 --- a/test/json/json_generator_test.rb +++ b/test/json/json_generator_test.rb @@ -822,7 +822,7 @@ def test_fragment def test_json_generate_as_json_convert_to_proc object = Object.new - assert_equal object.object_id.to_json, JSON.generate(object, strict: true, as_json: :object_id) + assert_equal object.object_id.to_json, JSON.generate(object, strict: true, as_json: -> (o, is_key) { o.object_id }) end def assert_float_roundtrip(expected, actual) From 4d9068c1a6bee90235dd4aa8bdfb7c4416688de6 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Thu, 18 Sep 2025 21:31:40 +0200 Subject: [PATCH 3/9] Refactor Truffle generator type checks --- lib/json/truffle_ruby/generator.rb | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/lib/json/truffle_ruby/generator.rb b/lib/json/truffle_ruby/generator.rb index 0e935d50a..2d01ad1b5 100644 --- a/lib/json/truffle_ruby/generator.rb +++ b/lib/json/truffle_ruby/generator.rb @@ -47,6 +47,14 @@ module Generator SCRIPT_SAFE_ESCAPE_PATTERN = /[\/"\\\x0-\x1f\u2028-\u2029]/ + def self.native_type?(value) # :nodoc: + (false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value || Fragment === value) + end + + def self.native_key?(key) # :nodoc: + (Symbol === key || String === key) + end + # Convert a UTF8 encoded Ruby string _string_ to a JSON string, encoded with # UTF16 big endian characters as \u????, and return it. def self.utf8_to_json(string, script_safe = false) # :nodoc: @@ -448,10 +456,10 @@ def to_json(state = nil, *) state = State.from_state(state) if state if state&.strict? value = self - if state.strict? && !(false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value || Fragment === value) + if state.strict? && !Generator.native_type?(value) if state.as_json value = state.as_json.call(value, false) - unless false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value || Fragment === value + unless Generator.native_type?(value) raise GeneratorError.new("#{value.class} returned by #{state.as_json} not allowed in JSON", value) end value.to_json(state) @@ -509,12 +517,12 @@ def json_transform(state) end result << state.indent * depth if indent - if state.strict? && !(Symbol === key || String === key) + if state.strict? && !Generator.native_key?(key) if state.as_json key = state.as_json.call(key, true) end - unless Symbol === key || String === key + unless Generator.native_key?(key) raise GeneratorError.new("#{key.class} not allowed as object key in JSON", value) end end @@ -527,10 +535,10 @@ def json_transform(state) end result = +"#{result}#{key_json}#{state.space_before}:#{state.space}" - if state.strict? && !(false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value || Fragment === value) + if state.strict? && !Generator.native_type?(value) if state.as_json value = state.as_json.call(value, false) - unless false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value || Fragment === value + unless Generator.native_type?(value) raise GeneratorError.new("#{value.class} returned by #{state.as_json} not allowed in JSON", value) end result << value.to_json(state) @@ -588,10 +596,10 @@ def json_transform(state) each { |value| result << delim unless first result << state.indent * depth if indent - if state.strict? && !(false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value || Fragment === value || Symbol == value) + if state.strict? && !Generator.native_type?(value) if state.as_json value = state.as_json.call(value, false) - unless false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value || Fragment === value || Symbol === value + unless Generator.native_type?(value) raise GeneratorError.new("#{value.class} returned by #{state.as_json} not allowed in JSON", value) end result << value.to_json(state) From efd67e7dfd32ba31cfe0c764a4825816f0e56a28 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Fri, 19 Sep 2025 00:05:04 +0200 Subject: [PATCH 4/9] Update changelog --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 33929954f..60273d26e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -26,6 +26,7 @@ * Fix `JSON::Coder` to also invoke block for hash keys that aren't strings nor symbols. * Fix `JSON.unsafe_load` usage with proc * Fix the parser to more consistently reject invalid UTF-16 surogate pairs. +* Stop defining `String.json_create`, `String#to_json_raw`, `String#to_json_raw_object` when `json/add` isn't loaded. ### 2025-07-28 (2.13.2) From 0a9478a715ea766d627ff3a72cf7a4f033fe536f Mon Sep 17 00:00:00 2001 From: Scott Myron Date: Thu, 18 Sep 2025 21:15:40 -0500 Subject: [PATCH 5/9] implement a better fix for an out of bounds exception --- java/src/json/ext/SWARBasicStringEncoder.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/java/src/json/ext/SWARBasicStringEncoder.java b/java/src/json/ext/SWARBasicStringEncoder.java index 08cd2e473..02a3bfcde 100644 --- a/java/src/json/ext/SWARBasicStringEncoder.java +++ b/java/src/json/ext/SWARBasicStringEncoder.java @@ -23,9 +23,13 @@ void encode(ByteList src) throws IOException { int beg = 0; int pos = 0; - ByteBuffer bb = ByteBuffer.wrap(ptrBytes, 0, len); - while (ptr + pos + 8 <= len) { - long x = bb.getLong(ptr + pos); + // There are optimizations in JRuby where ptr > 0 will be true. For example, if we + // slice a string, the underlying byte array is the same, but the + // begin index and real size are different. When reading from the ptrBytes + // array, we need to always add ptr to the index. + ByteBuffer bb = ByteBuffer.wrap(ptrBytes, ptr, len); + while (pos + 8 <= len) { + long x = bb.getLong(pos); if (skipChunk(x)) { pos += 8; continue; @@ -43,8 +47,8 @@ void encode(ByteList src) throws IOException { } } - if (ptr + pos + 4 <= len) { - int x = bb.getInt(ptr + pos); + if (pos + 4 <= len) { + int x = bb.getInt(pos); if (skipChunk(x)) { pos += 4; } From a2aa1cb3d0de7b7a81253191e9e7e0baa39f0bf4 Mon Sep 17 00:00:00 2001 From: Charles Oliver Nutter Date: Sat, 20 Sep 2025 04:25:34 +0530 Subject: [PATCH 6/9] Add JRuby build output to CLEAN and CLOBBER --- Rakefile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Rakefile b/Rakefile index c1768853c..64a440db7 100644 --- a/Rakefile +++ b/Rakefile @@ -91,6 +91,12 @@ JAVA_CLASSES = [] JRUBY_PARSER_JAR = File.expand_path("lib/json/ext/parser.jar") JRUBY_GENERATOR_JAR = File.expand_path("lib/json/ext/generator.jar") +CLEAN.concat FileList["java/src/**/*.class"] +CLEAN << JRUBY_PARSER_JAR +CLEAN << JRUBY_GENERATOR_JAR + +CLOBBER << JAVA_PARSER_SRC + which = lambda { |c| w = `which #{c}` break w.chomp unless w.empty? From b1cc12605b5b5331b9c6c36774c37464d694b6cd Mon Sep 17 00:00:00 2001 From: Charles Oliver Nutter Date: Sat, 20 Sep 2025 04:26:03 +0530 Subject: [PATCH 7/9] Use --release flag on Java 9+ The check for the modules dir here indicates whether we are on JDK 9+, since 1.8 did not have modules. Fixes #862. See #859 for some background. --- Rakefile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index 64a440db7..9681be0d7 100644 --- a/Rakefile +++ b/Rakefile @@ -146,7 +146,11 @@ if defined?(RUBY_ENGINE) and RUBY_ENGINE == 'jruby' classpath = (Dir['java/lib/*.jar'] << 'java/src' << JRUBY_JAR) * path_separator obj = src.sub(/\.java\Z/, '.class') file obj => src do - sh 'javac', '-classpath', classpath, '-source', '1.8', '-target', '1.8', src + if File.exist?(File.join(ENV['JAVA_HOME'], "lib", "modules")) + sh 'javac', '-classpath', classpath, '--release', '8', src + else + sh 'javac', '-classpath', classpath, '-source', '1.8', '-target', '1.8', src + end end JAVA_CLASSES << obj end From 3d1ed18b65c7581874ef65a7e197ef7b80b5ae49 Mon Sep 17 00:00:00 2001 From: Charles Oliver Nutter Date: Sat, 20 Sep 2025 04:37:36 +0530 Subject: [PATCH 8/9] Add macos platform for JRuby CI --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4070118b..b9b6bd96e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,7 @@ jobs: - { os: ubuntu-latest , ruby: 3.4, env: "JSON_DEBUG=1" } - { os: macos-13, ruby: 3.4 } - { os: windows-latest , ruby: mswin } # ruby/ruby windows CI + - { os: macos-latest , ruby: jruby-9.4 } # Ruby 3.1 - { os: ubuntu-latest , ruby: jruby-9.4 } # Ruby 3.1 - { os: macos-latest , ruby: truffleruby-head } - { os: ubuntu-latest , ruby: truffleruby-head } From 4abfad090d8398bfd30b4fcb253595151eb7f397 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Mon, 22 Sep 2025 12:02:24 +0200 Subject: [PATCH 9/9] Release 2.15.0 --- CHANGES.md | 2 ++ lib/json/version.rb | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 60273d26e..747b8059b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,8 @@ ### Unreleased +### 2025-09-22 (2.15.0) + * `JSON::Coder` callback now receive a second argument to convey whether the object is a hash key. * Tuned the floating point number generator to not use scientific notation as agressively. diff --git a/lib/json/version.rb b/lib/json/version.rb index 9c928e394..e2db923e3 100644 --- a/lib/json/version.rb +++ b/lib/json/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module JSON - VERSION = '2.14.1' + VERSION = '2.15.0' end