From 312235a4999dc512505fffbe6da1148b97a1a1f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Barri=C3=A9?= Date: Fri, 22 Aug 2025 11:54:18 +0200 Subject: [PATCH 01/12] [ruby/json] Test behavior of parsing a too big Float https://github.com/ruby/json/commit/8510ea5c1a --- test/json/json_parser_test.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/json/json_parser_test.rb b/test/json/json_parser_test.rb index 6455d2971ae3c0..3c60fec8753ea5 100644 --- a/test/json/json_parser_test.rb +++ b/test/json/json_parser_test.rb @@ -128,6 +128,7 @@ def test_parse_numbers assert_equal(1.0/0, parse('Infinity', :allow_nan => true)) assert_raise(ParserError) { parse('-Infinity') } assert_equal(-1.0/0, parse('-Infinity', :allow_nan => true)) + capture_output { assert_equal(Float::INFINITY, parse("23456789012E666")) } end def test_parse_bigdecimals From 92bd1d9b5e0e687bac6f5690e62245eed3af791c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Barri=C3=A9?= Date: Fri, 22 Aug 2025 11:56:36 +0200 Subject: [PATCH 02/12] [ruby/json] Remove too big Float from fixture to avoid warning https://github.com/ruby/json/commit/e881e55e83 --- test/json/fixtures/pass1.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/json/fixtures/pass1.json b/test/json/fixtures/pass1.json index 7828fcc1374e9f..fa9058b1366bb5 100644 --- a/test/json/fixtures/pass1.json +++ b/test/json/fixtures/pass1.json @@ -12,7 +12,7 @@ "real": -9876.543210, "e": 0.123456789e-12, "E": 1.234567890E+34, - "": 23456789012E666, + "": 23456789012E66, "zero": 0, "one": 1, "space": " ", From 66815a4c0c1141a30ec7edce7ae97b9788ec0a46 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Sat, 23 Aug 2025 16:05:07 +0200 Subject: [PATCH 03/12] [ruby/json] Silence ractor experimental warnings https://github.com/ruby/json/commit/e77f610b21 --- test/json/ractor_test.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/json/ractor_test.rb b/test/json/ractor_test.rb index 0ebdb0e91a1cfc..53e1099ce9b76c 100644 --- a/test/json/ractor_test.rb +++ b/test/json/ractor_test.rb @@ -20,6 +20,7 @@ module RactorBackport def test_generate pid = fork do + Warning[:experimental] = false r = Ractor.new do json = JSON.generate({ 'a' => 2, From 4d6b1241f628392cad79c8aef1477445514135f2 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Sat, 23 Aug 2025 21:04:59 +0200 Subject: [PATCH 04/12] [ruby/json] Extract `fbuffer_append_str_repeat` function https://github.com/ruby/json/commit/12656777dc --- ext/json/fbuffer/fbuffer.h | 15 +++++++++++++-- ext/json/generator/generator.c | 21 +++++---------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/ext/json/fbuffer/fbuffer.h b/ext/json/fbuffer/fbuffer.h index d32371476c85a2..dc40dec7f57981 100644 --- a/ext/json/fbuffer/fbuffer.h +++ b/ext/json/fbuffer/fbuffer.h @@ -197,11 +197,22 @@ static void fbuffer_append_str(FBuffer *fb, VALUE str) const char *newstr = StringValuePtr(str); unsigned long len = RSTRING_LEN(str); - RB_GC_GUARD(str); - fbuffer_append(fb, newstr, len); } +static void fbuffer_append_str_repeat(FBuffer *fb, VALUE str, size_t repeat) +{ + unsigned long len = RSTRING_LEN(str); + + size_t total = repeat * len; + fbuffer_inc_capa(fb, total); + + while (repeat) { + fbuffer_append_str(fb, str); + repeat--; + } +} + static inline void fbuffer_append_char(FBuffer *fb, char newchr) { fbuffer_inc_capa(fb, 1); diff --git a/ext/json/generator/generator.c b/ext/json/generator/generator.c index 9c6ed930495a51..af6ba16e3fe67f 100644 --- a/ext/json/generator/generator.c +++ b/ext/json/generator/generator.c @@ -1016,16 +1016,13 @@ json_object_i(VALUE key, VALUE val, VALUE _arg) JSON_Generator_State *state = data->state; long depth = state->depth; - int j; if (arg->iter > 0) fbuffer_append_char(buffer, ','); if (RB_UNLIKELY(data->state->object_nl)) { fbuffer_append_str(buffer, data->state->object_nl); } if (RB_UNLIKELY(data->state->indent)) { - for (j = 0; j < depth; j++) { - fbuffer_append_str(buffer, data->state->indent); - } + fbuffer_append_str_repeat(buffer, data->state->indent, depth); } VALUE key_to_s; @@ -1071,7 +1068,6 @@ static inline long increase_depth(struct generate_json_data *data) static void generate_json_object(FBuffer *buffer, struct generate_json_data *data, VALUE obj) { - int j; long depth = increase_depth(data); if (RHASH_SIZE(obj) == 0) { @@ -1092,9 +1088,7 @@ static void generate_json_object(FBuffer *buffer, struct generate_json_data *dat if (RB_UNLIKELY(data->state->object_nl)) { fbuffer_append_str(buffer, data->state->object_nl); if (RB_UNLIKELY(data->state->indent)) { - for (j = 0; j < depth; j++) { - fbuffer_append_str(buffer, data->state->indent); - } + fbuffer_append_str_repeat(buffer, data->state->indent, depth); } } fbuffer_append_char(buffer, '}'); @@ -1102,7 +1096,6 @@ static void generate_json_object(FBuffer *buffer, struct generate_json_data *dat static void generate_json_array(FBuffer *buffer, struct generate_json_data *data, VALUE obj) { - int i, j; long depth = increase_depth(data); if (RARRAY_LEN(obj) == 0) { @@ -1113,15 +1106,13 @@ static void generate_json_array(FBuffer *buffer, struct generate_json_data *data fbuffer_append_char(buffer, '['); if (RB_UNLIKELY(data->state->array_nl)) fbuffer_append_str(buffer, data->state->array_nl); - for (i = 0; i < RARRAY_LEN(obj); i++) { + for (int i = 0; i < RARRAY_LEN(obj); i++) { if (i > 0) { fbuffer_append_char(buffer, ','); if (RB_UNLIKELY(data->state->array_nl)) fbuffer_append_str(buffer, data->state->array_nl); } if (RB_UNLIKELY(data->state->indent)) { - for (j = 0; j < depth; j++) { - fbuffer_append_str(buffer, data->state->indent); - } + fbuffer_append_str_repeat(buffer, data->state->indent, depth); } generate_json(buffer, data, RARRAY_AREF(obj, i)); } @@ -1129,9 +1120,7 @@ static void generate_json_array(FBuffer *buffer, struct generate_json_data *data if (RB_UNLIKELY(data->state->array_nl)) { fbuffer_append_str(buffer, data->state->array_nl); if (RB_UNLIKELY(data->state->indent)) { - for (j = 0; j < depth; j++) { - fbuffer_append_str(buffer, data->state->indent); - } + fbuffer_append_str_repeat(buffer, data->state->indent, depth); } } fbuffer_append_char(buffer, ']'); From f6823936fa2ebfc4372e44499237235ae17f0bd9 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Sun, 24 Aug 2025 15:22:31 +0200 Subject: [PATCH 05/12] [ruby/json] parser.c: Remove useless dereference https://github.com/ruby/json/commit/2d63648c0a --- ext/json/parser/parser.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index 1e6ee753f0cf4b..496a769206bc22 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -975,7 +975,7 @@ static inline bool FORCE_INLINE string_scan(JSON_ParserState *state) if (RB_UNLIKELY(string_scan_table[(unsigned char)*state->cursor])) { return 1; } - *state->cursor++; + state->cursor++; } return 0; } From 97b5df11435d60d4dcdeb9474275cfc694d43b1e Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Sun, 24 Aug 2025 17:39:10 +0200 Subject: [PATCH 06/12] [ruby/json] Optimize `fbuffer_append_str_repeat` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Helps with pretty printting performance: ``` == Encoding activitypub.json (52595 bytes) ruby 3.4.2 (2025-02-15 revision https://github.com/ruby/json/commit/d2930f8e7a) +YJIT +PRISM [arm64-darwin24] Warming up -------------------------------------- after 1.746k i/100ms Calculating ------------------------------------- after 17.481k (± 1.0%) i/s (57.20 μs/i) - 89.046k in 5.094341s Comparison: before: 16038.4 i/s after: 17481.1 i/s - 1.09x faster == Encoding citm_catalog.json (500298 bytes) ruby 3.4.2 (2025-02-15 revision https://github.com/ruby/json/commit/d2930f8e7a) +YJIT +PRISM [arm64-darwin24] Warming up -------------------------------------- after 60.000 i/100ms Calculating ------------------------------------- after 608.157 (± 2.3%) i/s (1.64 ms/i) - 3.060k in 5.034238s Comparison: before: 525.3 i/s after: 608.2 i/s - 1.16x faster == Encoding twitter.json (466906 bytes) ruby 3.4.2 (2025-02-15 revision https://github.com/ruby/json/commit/d2930f8e7a) +YJIT +PRISM [arm64-darwin24] Warming up -------------------------------------- after 160.000 i/100ms Calculating ------------------------------------- after 1.606k (± 0.5%) i/s (622.70 μs/i) - 8.160k in 5.081406s Comparison: before: 1410.3 i/s after: 1605.9 i/s - 1.14x faster ``` https://github.com/ruby/json/commit/f0dda861c5 --- ext/json/fbuffer/fbuffer.h | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/ext/json/fbuffer/fbuffer.h b/ext/json/fbuffer/fbuffer.h index dc40dec7f57981..247a0de470b75c 100644 --- a/ext/json/fbuffer/fbuffer.h +++ b/ext/json/fbuffer/fbuffer.h @@ -169,12 +169,17 @@ static inline void fbuffer_inc_capa(FBuffer *fb, unsigned long requested) } } -static void fbuffer_append(FBuffer *fb, const char *newstr, unsigned long len) +static inline void fbuffer_append_reserved(FBuffer *fb, const char *newstr, unsigned long len) +{ + MEMCPY(fb->ptr + fb->len, newstr, char, len); + fbuffer_consumed(fb, len); +} + +static inline void fbuffer_append(FBuffer *fb, const char *newstr, unsigned long len) { if (len > 0) { fbuffer_inc_capa(fb, len); - MEMCPY(fb->ptr + fb->len, newstr, char, len); - fbuffer_consumed(fb, len); + fbuffer_append_reserved(fb, newstr, len); } } @@ -202,13 +207,15 @@ static void fbuffer_append_str(FBuffer *fb, VALUE str) static void fbuffer_append_str_repeat(FBuffer *fb, VALUE str, size_t repeat) { + const char *newstr = StringValuePtr(str); unsigned long len = RSTRING_LEN(str); - size_t total = repeat * len; - fbuffer_inc_capa(fb, total); - + fbuffer_inc_capa(fb, repeat * len); while (repeat) { - fbuffer_append_str(fb, str); +#ifdef JSON_DEBUG + fb->requested = len; +#endif + fbuffer_append_reserved(fb, newstr, len); repeat--; } } From a062b9a594ad8d42b0e347da786d42818865abcf Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Mon, 25 Aug 2025 09:21:05 +0200 Subject: [PATCH 07/12] [ruby/json] Remove reference to fast_generate https://github.com/ruby/json/commit/19bcfdd8d8 --- ext/json/lib/json/common.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/json/lib/json/common.rb b/ext/json/lib/json/common.rb index e99d152a884898..2859a8c63fea7b 100644 --- a/ext/json/lib/json/common.rb +++ b/ext/json/lib/json/common.rb @@ -391,7 +391,7 @@ def load_file!(filespec, opts = nil) # # Returns a \String containing the generated \JSON data. # - # See also JSON.fast_generate, JSON.pretty_generate. + # See also JSON.pretty_generate. # # Argument +obj+ is the Ruby object to be converted to \JSON. # From d325e3ed706ec356c9439824ed82cbcdb4487859 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Mon, 25 Aug 2025 09:39:47 +0200 Subject: [PATCH 08/12] [ruby/json] Improve generation options documentation https://github.com/ruby/json/commit/3187c88c06 --- ext/json/lib/json.rb | 22 ++++++++++++++++++++++ ext/json/lib/json/ext/generator/state.rb | 16 ++-------------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/ext/json/lib/json.rb b/ext/json/lib/json.rb index 0ebff2f948af56..43d96afa95835c 100644 --- a/ext/json/lib/json.rb +++ b/ext/json/lib/json.rb @@ -307,6 +307,25 @@ # # --- # +# Option +allow_duplicate_key+ (boolean) specifies whether +# hashes with duplicate keys should be allowed or produce an error. +# defaults to emit a deprecation warning. +# +# With the default, (not set): +# Warning[:deprecated] = true +# JSON.generate({ foo: 1, "foo" => 2 }) +# # warning: detected duplicate key "foo" in {foo: 1, "foo" => 2}. +# # This will raise an error in json 3.0 unless enabled via `allow_duplicate_key: true` +# # => '{"foo":1,"foo":2}' +# +# With false +# JSON.generate({ foo: 1, "foo" => 2 }, allow_duplicate_key: false) +# # detected duplicate key "foo" in {foo: 1, "foo" => 2} (JSON::GeneratorError) +# +# In version 3.0, false will become the default. +# +# --- +# # Option +max_nesting+ (\Integer) specifies the maximum nesting depth # in +obj+; defaults to +100+. # @@ -384,6 +403,9 @@ # # == \JSON Additions # +# Note that JSON Additions must only be used with trusted data, and is +# deprecated. +# # When you "round trip" a non-\String object from Ruby to \JSON and back, # you have a new \String, instead of the object you began with: # ruby0 = Range.new(0, 2) diff --git a/ext/json/lib/json/ext/generator/state.rb b/ext/json/lib/json/ext/generator/state.rb index d40c3b5ec35a30..2ec2daa2029541 100644 --- a/ext/json/lib/json/ext/generator/state.rb +++ b/ext/json/lib/json/ext/generator/state.rb @@ -8,20 +8,8 @@ class State # # Instantiates a new State object, configured by _opts_. # - # _opts_ can have the following keys: - # - # * *indent*: a string used to indent levels (default: ''), - # * *space*: a string that is put after, a : or , delimiter (default: ''), - # * *space_before*: a string that is put before a : pair delimiter (default: ''), - # * *object_nl*: a string that is put at the end of a JSON object (default: ''), - # * *array_nl*: a string that is put at the end of a JSON array (default: ''), - # * *allow_nan*: true if NaN, Infinity, and -Infinity should be - # generated, otherwise an exception is thrown, if these values are - # encountered. This options defaults to false. - # * *ascii_only*: true if only ASCII characters should be generated. This - # option defaults to false. - # * *buffer_initial_length*: sets the initial length of the generator's - # internal buffer. + # Argument +opts+, if given, contains a \Hash of options for the generation. + # See {Generating Options}[#module-JSON-label-Generating+Options]. def initialize(opts = nil) if opts && !opts.empty? configure(opts) From c3a80ca58226f588ef393ab5ae1de304eabf9a9d Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Sat, 23 Aug 2025 16:06:38 +0200 Subject: [PATCH 09/12] Fix `JSON.generate` `strict: true` mode to also restrict hash keys --- ext/json/generator/generator.c | 3 +++ test/json/json_generator_test.rb | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/ext/json/generator/generator.c b/ext/json/generator/generator.c index af6ba16e3fe67f..c71e2f28a758fd 100644 --- a/ext/json/generator/generator.c +++ b/ext/json/generator/generator.c @@ -1038,6 +1038,9 @@ json_object_i(VALUE key, VALUE val, VALUE _arg) key_to_s = rb_sym2str(key); break; default: + if (data->state->strict) { + raise_generator_error(key, "%"PRIsVALUE" not allowed in JSON", rb_funcall(key, i_to_s, 0)); + } key_to_s = rb_convert_type(key, T_STRING, "String", "to_s"); break; } diff --git a/test/json/json_generator_test.rb b/test/json/json_generator_test.rb index 914b3f4ed0a904..963350ea49f6db 100755 --- a/test/json/json_generator_test.rb +++ b/test/json/json_generator_test.rb @@ -404,6 +404,18 @@ def test_json_generate_unsupported_types assert_raise JSON::GeneratorError do generate(Object.new, strict: true) end + + assert_raise JSON::GeneratorError do + generate([Object.new], strict: true) + end + + assert_raise JSON::GeneratorError do + generate({ "key" => Object.new }, strict: true) + end + + assert_raise JSON::GeneratorError do + generate({ Object.new => "value" }, strict: true) + end end def test_nesting From 0e0f0dfd070fc156ec74c58f44d86a884a0580e0 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Sat, 23 Aug 2025 16:58:54 +0200 Subject: [PATCH 10/12] Fix `JSON::Coder` to cast non-string keys. --- ext/json/generator/generator.c | 11 ++++++++++- test/json/json_coder_test.rb | 12 ++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/ext/json/generator/generator.c b/ext/json/generator/generator.c index c71e2f28a758fd..52dcd24f0e9dab 100644 --- a/ext/json/generator/generator.c +++ b/ext/json/generator/generator.c @@ -1026,6 +1026,9 @@ json_object_i(VALUE key, VALUE val, VALUE _arg) } VALUE key_to_s; + bool as_json_called = false; + + start: switch (rb_type(key)) { case T_STRING: if (RB_LIKELY(RBASIC_CLASS(key) == rb_cString)) { @@ -1039,7 +1042,13 @@ json_object_i(VALUE key, VALUE val, VALUE _arg) break; default: if (data->state->strict) { - raise_generator_error(key, "%"PRIsVALUE" not allowed in JSON", rb_funcall(key, i_to_s, 0)); + if (RTEST(data->state->as_json) && !as_json_called) { + key = rb_proc_call_with_block(data->state->as_json, 1, &key, Qnil); + as_json_called = true; + goto start; + } else { + raise_generator_error(key, "%"PRIsVALUE" not allowed as object key in JSON", CLASS_OF(key)); + } } key_to_s = rb_convert_type(key, T_STRING, "String", "to_s"); break; diff --git a/test/json/json_coder_test.rb b/test/json/json_coder_test.rb index 9861181910cefd..fc4aba296858ae 100755 --- a/test/json/json_coder_test.rb +++ b/test/json/json_coder_test.rb @@ -18,6 +18,18 @@ def test_json_coder_with_proc_with_unsupported_value assert_raise(JSON::GeneratorError) { coder.dump([Object.new]) } end + def test_json_coder_hash_key + obj = Object.new + coder = JSON::Coder.new(&:to_s) + assert_equal %({#{obj.to_s.inspect}:1}), coder.dump({ obj => 1 }) + + coder = JSON::Coder.new { 42 } + error = assert_raise JSON::GeneratorError do + coder.dump({ obj => 1 }) + end + assert_equal "Integer not allowed as object key in JSON", error.message + end + def test_json_coder_options coder = JSON::Coder.new(array_nl: "\n") do |object| 42 From d9e9a667a8c8fb6f57611c68b45eaf1f2c39fca1 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Sat, 23 Aug 2025 19:57:14 +0200 Subject: [PATCH 11/12] JSON.generate: warn or raise on duplicated key Because both strings and symbols keys are serialized the same, it always has been possible to generate documents with duplicated keys: ```ruby >> puts JSON.generate({ foo: 1, "foo" => 2 }) {"foo":1,"foo":2} ``` This is pretty much always a mistake and can cause various issues because it's not guaranteed how various JSON parsers will handle this. Until now I didn't think it was possible to catch such case without tanking performance, hence why I only made the parser more strict. But I finally found a way to check for duplicated keys cheaply enough. --- ext/json/fbuffer/fbuffer.h | 8 +++ ext/json/generator/generator.c | 73 ++++++++++++++++++++++-- ext/json/lib/json/common.rb | 19 ++++++ ext/json/lib/json/ext/generator/state.rb | 5 ++ ext/json/parser/parser.c | 2 +- test/json/json_generator_test.rb | 38 ++++++++++++ 6 files changed, 138 insertions(+), 7 deletions(-) diff --git a/ext/json/fbuffer/fbuffer.h b/ext/json/fbuffer/fbuffer.h index 247a0de470b75c..d5fd8ac6d71f78 100644 --- a/ext/json/fbuffer/fbuffer.h +++ b/ext/json/fbuffer/fbuffer.h @@ -24,6 +24,14 @@ typedef unsigned char _Bool; #endif #endif +#ifndef NOINLINE +#if defined(__has_attribute) && __has_attribute(noinline) +#define NOINLINE() __attribute__((noinline)) +#else +#define NOINLINE() +#endif +#endif + #ifndef RB_UNLIKELY #define RB_UNLIKELY(expr) expr #endif diff --git a/ext/json/generator/generator.c b/ext/json/generator/generator.c index 52dcd24f0e9dab..d810927c58087c 100644 --- a/ext/json/generator/generator.c +++ b/ext/json/generator/generator.c @@ -9,6 +9,12 @@ /* ruby api and some helpers */ +enum duplicate_key_action { + JSON_DEPRECATED = 0, + JSON_IGNORE, + JSON_RAISE, +}; + typedef struct JSON_Generator_StateStruct { VALUE indent; VALUE space; @@ -21,6 +27,8 @@ typedef struct JSON_Generator_StateStruct { long depth; long buffer_initial_length; + enum duplicate_key_action on_duplicate_key; + bool allow_nan; bool ascii_only; bool script_safe; @@ -34,7 +42,7 @@ typedef struct JSON_Generator_StateStruct { static VALUE mJSON, cState, cFragment, eGeneratorError, eNestingError, Encoding_UTF_8; static ID i_to_s, i_to_json, i_new, i_pack, i_unpack, i_create_id, i_extend, i_encode; -static VALUE sym_indent, sym_space, sym_space_before, sym_object_nl, sym_array_nl, sym_max_nesting, sym_allow_nan, +static VALUE sym_indent, sym_space, sym_space_before, sym_object_nl, sym_array_nl, sym_max_nesting, sym_allow_nan, sym_allow_duplicate_key, sym_ascii_only, sym_depth, sym_buffer_initial_length, sym_script_safe, sym_escape_slash, sym_strict, sym_as_json; @@ -987,8 +995,11 @@ static inline VALUE vstate_get(struct generate_json_data *data) } struct hash_foreach_arg { + VALUE hash; struct generate_json_data *data; - int iter; + int first_key_type; + bool first; + bool mixed_keys_encountered; }; static VALUE @@ -1006,6 +1017,22 @@ convert_string_subclass(VALUE key) return key_to_s; } +NOINLINE() +static void +json_inspect_hash_with_mixed_keys(struct hash_foreach_arg *arg) +{ + if (arg->mixed_keys_encountered) { + return; + } + arg->mixed_keys_encountered = true; + + JSON_Generator_State *state = arg->data->state; + if (state->on_duplicate_key != JSON_IGNORE) { + VALUE do_raise = state->on_duplicate_key == JSON_RAISE ? Qtrue : Qfalse; + rb_funcall(mJSON, rb_intern("on_mixed_keys_hash"), 2, arg->hash, do_raise); + } +} + static int json_object_i(VALUE key, VALUE val, VALUE _arg) { @@ -1016,8 +1043,16 @@ json_object_i(VALUE key, VALUE val, VALUE _arg) JSON_Generator_State *state = data->state; long depth = state->depth; + int key_type = rb_type(key); + + if (arg->first) { + arg->first = false; + arg->first_key_type = key_type; + } + else { + fbuffer_append_char(buffer, ','); + } - if (arg->iter > 0) fbuffer_append_char(buffer, ','); if (RB_UNLIKELY(data->state->object_nl)) { fbuffer_append_str(buffer, data->state->object_nl); } @@ -1029,8 +1064,12 @@ json_object_i(VALUE key, VALUE val, VALUE _arg) bool as_json_called = false; start: - switch (rb_type(key)) { + switch (key_type) { case T_STRING: + if (RB_UNLIKELY(arg->first_key_type != T_STRING)) { + json_inspect_hash_with_mixed_keys(arg); + } + if (RB_LIKELY(RBASIC_CLASS(key) == rb_cString)) { key_to_s = key; } else { @@ -1038,12 +1077,17 @@ json_object_i(VALUE key, VALUE val, VALUE _arg) } break; case T_SYMBOL: + if (RB_UNLIKELY(arg->first_key_type != T_SYMBOL)) { + json_inspect_hash_with_mixed_keys(arg); + } + key_to_s = rb_sym2str(key); break; 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_type = rb_type(key); as_json_called = true; goto start; } else { @@ -1064,7 +1108,6 @@ json_object_i(VALUE key, VALUE val, VALUE _arg) if (RB_UNLIKELY(state->space)) fbuffer_append_str(buffer, data->state->space); generate_json(buffer, data, val); - arg->iter++; return ST_CONTINUE; } @@ -1091,8 +1134,9 @@ static void generate_json_object(FBuffer *buffer, struct generate_json_data *dat fbuffer_append_char(buffer, '{'); struct hash_foreach_arg arg = { + .hash = obj, .data = data, - .iter = 0, + .first = true, }; rb_hash_foreach(obj, json_object_i, (VALUE)&arg); @@ -1794,6 +1838,19 @@ static VALUE cState_ascii_only_set(VALUE self, VALUE enable) return Qnil; } +static VALUE cState_allow_duplicate_key_p(VALUE self) +{ + GET_STATE(self); + switch (state->on_duplicate_key) { + case JSON_IGNORE: + return Qtrue; + case JSON_DEPRECATED: + return Qnil; + case JSON_RAISE: + return Qfalse; + } +} + /* * call-seq: depth * @@ -1883,6 +1940,7 @@ static int configure_state_i(VALUE key, VALUE val, VALUE _arg) else if (key == sym_script_safe) { state->script_safe = RTEST(val); } else if (key == sym_escape_slash) { state->script_safe = RTEST(val); } else if (key == sym_strict) { state->strict = RTEST(val); } + 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_write_value(data, &state->as_json, proc); @@ -2008,6 +2066,8 @@ void Init_generator(void) rb_define_method(cState, "generate", cState_generate, -1); rb_define_alias(cState, "generate_new", "generate"); // :nodoc: + rb_define_private_method(cState, "allow_duplicate_key?", cState_allow_duplicate_key_p, 0); + rb_define_singleton_method(cState, "generate", cState_m_generate, 3); VALUE mGeneratorMethods = rb_define_module_under(mGenerator, "GeneratorMethods"); @@ -2072,6 +2132,7 @@ void Init_generator(void) sym_escape_slash = ID2SYM(rb_intern("escape_slash")); sym_strict = ID2SYM(rb_intern("strict")); sym_as_json = ID2SYM(rb_intern("as_json")); + sym_allow_duplicate_key = ID2SYM(rb_intern("allow_duplicate_key")); usascii_encindex = rb_usascii_encindex(); utf8_encindex = rb_utf8_encindex(); diff --git a/ext/json/lib/json/common.rb b/ext/json/lib/json/common.rb index 2859a8c63fea7b..45200a83bceda6 100644 --- a/ext/json/lib/json/common.rb +++ b/ext/json/lib/json/common.rb @@ -186,6 +186,25 @@ def generator=(generator) # :nodoc: private + # Called from the extension when a hash has both string and symbol keys + def on_mixed_keys_hash(hash, do_raise) + set = {} + hash.each_key do |key| + key_str = key.to_s + + if set[key_str] + message = "detected duplicate key #{key_str.inspect} in #{hash.inspect}" + if do_raise + raise GeneratorError, message + else + deprecation_warning("#{message}.\nThis will raise an error in json 3.0 unless enabled via `allow_duplicate_key: true`") + end + else + set[key_str] = true + end + end + end + def deprecated_singleton_attr_accessor(*attrs) args = RUBY_VERSION >= "3.0" ? ", category: :deprecated" : "" attrs.each do |attr| diff --git a/ext/json/lib/json/ext/generator/state.rb b/ext/json/lib/json/ext/generator/state.rb index 2ec2daa2029541..1f56e6c682cc6d 100644 --- a/ext/json/lib/json/ext/generator/state.rb +++ b/ext/json/lib/json/ext/generator/state.rb @@ -56,6 +56,11 @@ def to_h buffer_initial_length: buffer_initial_length, } + allow_duplicate_key = allow_duplicate_key? + unless allow_duplicate_key.nil? + result[:allow_duplicate_key] = allow_duplicate_key + end + instance_variables.each do |iv| iv = iv.to_s[1..-1] result[iv.to_sym] = self[iv] diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index 496a769206bc22..e34f1999d5f191 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -1314,7 +1314,7 @@ static int parser_config_init_i(VALUE key, VALUE val, VALUE data) else if (key == sym_symbolize_names) { config->symbolize_names = RTEST(val); } else if (key == sym_freeze) { config->freeze = RTEST(val); } else if (key == sym_on_load) { config->on_load_proc = RTEST(val) ? val : Qfalse; } - else if (key == sym_allow_duplicate_key) { config->on_duplicate_key = RTEST(val) ? JSON_IGNORE : JSON_RAISE; } + else if (key == sym_allow_duplicate_key) { config->on_duplicate_key = RTEST(val) ? JSON_IGNORE : JSON_RAISE; } else if (key == sym_decimal_class) { if (RTEST(val)) { if (rb_respond_to(val, i_try_convert)) { diff --git a/test/json/json_generator_test.rb b/test/json/json_generator_test.rb index 963350ea49f6db..4315d109d8ec68 100755 --- a/test/json/json_generator_test.rb +++ b/test/json/json_generator_test.rb @@ -234,6 +234,24 @@ def test_state_defaults :space => "", :space_before => "", }.sort_by { |n,| n.to_s }, state.to_h.sort_by { |n,| n.to_s }) + + state = JSON::State.new(allow_duplicate_key: true) + assert_equal({ + :allow_duplicate_key => true, + :allow_nan => false, + :array_nl => "", + :as_json => false, + :ascii_only => false, + :buffer_initial_length => 1024, + :depth => 0, + :script_safe => false, + :strict => false, + :indent => "", + :max_nesting => 100, + :object_nl => "", + :space => "", + :space_before => "", + }.sort_by { |n,| n.to_s }, state.to_h.sort_by { |n,| n.to_s }) end def test_allow_nan @@ -828,4 +846,24 @@ def test_numbers_of_various_sizes assert_equal "[#{number}]", JSON.generate([number]) end end + + def test_generate_duplicate_keys_allowed + hash = { foo: 1, "foo" => 2 } + assert_equal %({"foo":1,"foo":2}), JSON.generate(hash, allow_duplicate_key: true) + end + + def test_generate_duplicate_keys_deprecated + hash = { foo: 1, "foo" => 2 } + assert_deprecated_warning(/allow_duplicate_key/) do + assert_equal %({"foo":1,"foo":2}), JSON.generate(hash) + end + end + + def test_generate_duplicate_keys_disallowed + hash = { foo: 1, "foo" => 2 } + error = assert_raise JSON::GeneratorError do + JSON.generate(hash, allow_duplicate_key: false) + end + assert_equal %(detected duplicate key "foo" in #{hash.inspect}), error.message + end end From 5ff7b2c582a56fe7d92248adf093fd278a334066 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Tue, 26 Aug 2025 11:01:15 -0700 Subject: [PATCH 12/12] [DOC] Add Ractor to NEWS --- NEWS.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/NEWS.md b/NEWS.md index 476d5ba88c5884..ea0f4c28bf0ce9 100644 --- a/NEWS.md +++ b/NEWS.md @@ -243,6 +243,23 @@ The following bundled gems are updated. ## Implementation improvements +### Ractor + +A lot of work has gone into making Ractors more stable, performant, and usable. These improvements bring Ractors implementation closer to leaving experimental status. + +* Performance improvements + * Frozen strings and the symbol table internally use a lock-free hash set + * Method cache lookups avoid locking in most cases + * Class (and geniv) instance variable access is faster and avoids locking + * Cache contention is avoided during object allocation + * `object_id` avoids locking in most cases +* Bug fixes and stability + * Fixed possible deadlocks when combining Ractors and Threads + * Fixed issues with require and autoload in a Ractor + * Fixed encoding/transcoding issues across Ractors + * Fixed race conditions in GC operations and method invalidation + * Fixed issues with processes forking after starting a Ractor + ## JIT * YJIT