From 52b842e87f4e3671ebea287098b426de7e0e6511 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Mon, 27 Jul 2026 19:38:48 +0200 Subject: [PATCH 01/18] Remove `j` and `jj` --- CHANGES.md | 2 ++ lib/json/common.rb | 30 ------------------------------ 2 files changed, 2 insertions(+), 30 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 3f455f7c4..97758091a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,8 @@ ### Unreleased +* Remove `Kernel#j` and `Kernel#jj` + ### 2026-07-31 (2.21.2) * Fix a use-after-free bug in `JSON::ResumableParser`. [GHSA-9hj4-r449-hfvc][CVE-2026-71847]. diff --git a/lib/json/common.rb b/lib/json/common.rb index fdcb860db..a8a83d9c9 100644 --- a/lib/json/common.rb +++ b/lib/json/common.rb @@ -1136,36 +1136,6 @@ def to_json(state = nil, *) module ::Kernel private - # Outputs _objs_ to STDOUT as JSON strings in the shortest form, that is in - # one line. - def j(*objs) - if RUBY_VERSION >= "3.0" - warn "Kernel#j is deprecated and will be removed in json 3.0.0", uplevel: 1, category: :deprecated - else - warn "Kernel#j is deprecated and will be removed in json 3.0.0", uplevel: 1 - end - - objs.each do |obj| - puts JSON.generate(obj, :allow_nan => true, :max_nesting => false) - end - nil - end - - # Outputs _objs_ to STDOUT as JSON strings in a pretty format, with - # indentation and over many lines. - def jj(*objs) - if RUBY_VERSION >= "3.0" - warn "Kernel#jj is deprecated and will be removed in json 3.0.0", uplevel: 1, category: :deprecated - else - warn "Kernel#jj is deprecated and will be removed in json 3.0.0", uplevel: 1 - end - - objs.each do |obj| - puts JSON.pretty_generate(obj, :allow_nan => true, :max_nesting => false) - end - nil - end - # If _object_ is string-like, parse the string and return the parsed result as # a Ruby data structure. Otherwise, generate a JSON text from the Ruby data # structure object and return it. From 773ec961e390b13853e2de72c69c3d41d1b2f1f0 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Mon, 27 Jul 2026 19:49:59 +0200 Subject: [PATCH 02/18] Make `allow_duplicate_key: false` the default --- CHANGES.md | 1 + ext/json/ext/generator/generator.c | 25 ++++---------------- ext/json/ext/parser/parser.c | 33 ++++----------------------- java/src/json/ext/Generator.java | 9 ++------ java/src/json/ext/GeneratorState.java | 24 ++----------------- java/src/json/ext/Parser.java | 20 ++-------------- lib/json.rb | 25 ++++++-------------- lib/json/common.rb | 9 ++------ lib/json/truffle_ruby/generator.rb | 16 ++++--------- test/json/json_generator_test.rb | 8 +------ test/json/json_parser_test.rb | 19 --------------- test/json/resumable_parser_test.rb | 13 ----------- 12 files changed, 30 insertions(+), 172 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 97758091a..ee3674f7b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ ### Unreleased +* The `allow_duplicate_key` option now defaults to `false`, for both parsing and generating JSON. * Remove `Kernel#j` and `Kernel#jj` ### 2026-07-31 (2.21.2) diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c index 6f473325e..a45fd3928 100644 --- a/ext/json/ext/generator/generator.c +++ b/ext/json/ext/generator/generator.c @@ -9,12 +9,6 @@ /* 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; @@ -27,8 +21,7 @@ typedef struct JSON_Generator_StateStruct { long depth; long buffer_initial_length; - enum duplicate_key_action on_duplicate_key; - + bool allow_duplicate_key; bool as_json_single_arg; bool allow_nan; bool ascii_only; @@ -956,9 +949,8 @@ json_inspect_hash_with_mixed_keys(struct hash_foreach_arg *arg) 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); + if (!state->allow_duplicate_key) { + rb_funcall(mJSON, rb_intern("on_mixed_keys_hash"), 1, arg->hash); } } @@ -1784,14 +1776,7 @@ static VALUE cState_sort_keys_set(VALUE self, VALUE value) 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; - default: - return Qfalse; - } + return state->allow_duplicate_key ? Qtrue : Qfalse; } /* @@ -1885,7 +1870,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_allow_duplicate_key) { state->allow_duplicate_key = RTEST(val); } 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; diff --git a/ext/json/ext/parser/parser.c b/ext/json/ext/parser/parser.c index 1443095b6..4a6344cb6 100644 --- a/ext/json/ext/parser/parser.c +++ b/ext/json/ext/parser/parser.c @@ -413,9 +413,9 @@ typedef struct JSON_ParserStruct { VALUE on_load_proc; VALUE decimal_class; ID decimal_method_id; - enum deprecatable_action on_duplicate_key; enum deprecatable_action on_comment; int max_nesting; + bool allow_duplicate_key; bool allow_nan; bool allow_trailing_comma; bool allow_control_characters; @@ -1219,17 +1219,6 @@ static VALUE json_find_duplicated_key(size_t count, const VALUE *pairs) return Qfalse; } -NOINLINE(static) void emit_duplicate_key_warning(JSON_ParserState *state, VALUE duplicate_key) -{ - VALUE message = rb_sprintf( - "detected duplicate key %"PRIsVALUE" in JSON object. This will raise an error in json 3.0 unless enabled via `allow_duplicate_key: true`", - rb_inspect(duplicate_key) - ); - - emit_parse_warning(RSTRING_PTR(message), state); - RB_GC_GUARD(message); -} - NORETURN(static) void raise_duplicate_key_error(JSON_ParserState *state, VALUE duplicate_key) { VALUE message = rb_sprintf( @@ -1250,23 +1239,9 @@ NORETURN(static) void raise_duplicate_key_error(JSON_ParserState *state, VALUE d NOINLINE(static) void json_on_duplicate_key(JSON_ParserState *state, JSON_ParserConfig *config, size_t count, const VALUE *pairs) { - switch (config->on_duplicate_key) { - case JSON_IGNORE: - return; - - case JSON_DEPRECATED: - // Only emit the first few deprecations to avoid spamming. - if (state->emitted_deprecations < MAX_DEPRECATIONS) { - state->emitted_deprecations++; - emit_duplicate_key_warning(state, json_find_duplicated_key(count, pairs)); - } - return; - - case JSON_RAISE: - raise_duplicate_key_error(state, json_find_duplicated_key(count, pairs)); - return; + if (!config->allow_duplicate_key) { + raise_duplicate_key_error(state, json_find_duplicated_key(count, pairs)); } - UNREACHABLE; } static inline VALUE json_decode_object(JSON_ParserState *state, JSON_ParserConfig *config, size_t count) @@ -2038,7 +2013,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) { parser_config_wb_write(self, &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->allow_duplicate_key = RTEST(val); } else if (key == sym_decimal_class) { if (RTEST(val)) { if (rb_respond_to(val, i_try_convert)) { diff --git a/java/src/json/ext/Generator.java b/java/src/json/ext/Generator.java index f1617a051..ce1b5b57e 100644 --- a/java/src/json/ext/Generator.java +++ b/java/src/json/ext/Generator.java @@ -553,13 +553,8 @@ private void report(ThreadContext context, Session session) { final GeneratorState state = session.getState(context); if (!state.getAllowDuplicateKey()) { - if (state.getDeprecateDuplicateKey()) { - IRubyObject args[] = new IRubyObject[]{hash, context.getRuntime().getFalse()}; - info.jsonModule.get().callMethod(context, "on_mixed_keys_hash", args); - } else { - IRubyObject args[] = new IRubyObject[]{hash, context.getRuntime().getTrue()}; - info.jsonModule.get().callMethod(context, "on_mixed_keys_hash", args); - } + IRubyObject args[] = new IRubyObject[]{hash}; + info.jsonModule.get().callMethod(context, "on_mixed_keys_hash", args); } } } diff --git a/java/src/json/ext/GeneratorState.java b/java/src/json/ext/GeneratorState.java index 2d20ea461..b5b28d2be 100644 --- a/java/src/json/ext/GeneratorState.java +++ b/java/src/json/ext/GeneratorState.java @@ -35,7 +35,6 @@ */ public class GeneratorState extends RubyObject { private boolean allowDuplicateKey = false; - private boolean deprecateDuplicateKey = true; private static IRubyObject defaultSortKeyProc; @@ -236,7 +235,6 @@ public IRubyObject initialize_copy(ThreadContext context, IRubyObject vOrig) { this.depth = orig.depth; this.allowDuplicateKey = orig.allowDuplicateKey; - this.deprecateDuplicateKey = orig.deprecateDuplicateKey; this.sortKeys = orig.sortKeys; return this; @@ -550,8 +548,6 @@ private ByteList prepareByteList(ThreadContext context, IRubyObject value) { public IRubyObject allow_duplicate_key_p(ThreadContext context) { if (allowDuplicateKey) { return context.runtime.getTrue(); - } else if (deprecateDuplicateKey) { - return context.runtime.getNil(); } return context.runtime.getFalse(); } @@ -560,10 +556,6 @@ public boolean getAllowDuplicateKey() { return allowDuplicateKey; } - public boolean getDeprecateDuplicateKey() { - return deprecateDuplicateKey; - } - /** * State#configure(opts) * @@ -608,11 +600,7 @@ public IRubyObject _configure(ThreadContext context, IRubyObject vOpts) { if (depth < 0) { throw context.runtime.newArgumentError("depth must be >= 0 (got: " + depth + ")"); } - - if (opts.hasKey("allow_duplicate_key")) { - this.allowDuplicateKey = opts.getBool("allow_duplicate_key", false); - this.deprecateDuplicateKey = false; - } + this.allowDuplicateKey = opts.getBool("allow_duplicate_key", false); sortKeys = normalizeSortKeys(context, opts.get("sort_keys")); @@ -645,15 +633,7 @@ public RubyHash to_h(ThreadContext context) { result.op_aset(context, runtime.newSymbol("depth"), depth_get(context)); result.op_aset(context, runtime.newSymbol("buffer_initial_length"), buffer_initial_length_get(context)); result.op_aset(context, runtime.newSymbol("sort_keys"), sort_keys_get(context)); - - if (this.allowDuplicateKey) { - if (!this.deprecateDuplicateKey) { - result.op_aset(context, runtime.newSymbol("allow_duplicate_key"), runtime.getTrue()); - } - } - else { - result.op_aset(context, runtime.newSymbol("allow_duplicate_key"), runtime.getFalse()); - } + result.op_aset(context, runtime.newSymbol("allow_duplicate_key"), allow_duplicate_key_p(context)); for (String name: getInstanceVariableNameList()) { result.op_aset(context, runtime.newSymbol(name.substring(1)), getInstanceVariables().getInstanceVariable(name)); diff --git a/java/src/json/ext/Parser.java b/java/src/json/ext/Parser.java index c54d1a7f9..6d5c18e06 100644 --- a/java/src/json/ext/Parser.java +++ b/java/src/json/ext/Parser.java @@ -50,7 +50,6 @@ public class Parser extends RubyObject { private boolean allowControlCharacters; private boolean allowInvalidEscape; private boolean allowDuplicateKey; - private boolean deprecateDuplicateKey; private boolean symbolizeNames; private boolean freeze; private RubyProc onLoadProc; @@ -109,13 +108,7 @@ public IRubyObject initialize(ThreadContext context, IRubyObject options) { this.allowInvalidEscape = opts.getBool("allow_invalid_escape", false); this.allowTrailingComma = opts.getBool("allow_trailing_comma", false); this.symbolizeNames = opts.getBool("symbolize_names", false); - if (opts.hasKey("allow_duplicate_key")) { - this.allowDuplicateKey = opts.getBool("allow_duplicate_key", false); - this.deprecateDuplicateKey = false; - } else { - this.allowDuplicateKey = false; - this.deprecateDuplicateKey = true; - } + this.allowDuplicateKey = opts.getBool("allow_duplicate_key", false); this.freeze = opts.getBool("freeze", false); this.onLoadProc = opts.getProc("on_load"); @@ -585,16 +578,7 @@ private void onDuplicateKey(IRubyObject key) { // match the C parser's message. String keyInspect = key.callMethod(context, "to_s") .callMethod(context, "inspect").asJavaString(); - if (config.deprecateDuplicateKey) { - if (emittedDeprecations < MAX_DEPRECATIONS) { - emittedDeprecations++; - context.runtime.getWarnings().warning( - "detected duplicate key " + keyInspect + " in JSON object. " + - "This will raise an error in json 3.0 unless enabled via `allow_duplicate_key: true`"); - } - } else { - throw newException(Utils.M_PARSER_ERROR, "duplicate key " + keyInspect); - } + throw newException(Utils.M_PARSER_ERROR, "duplicate key " + keyInspect); } private int parseDigits(long value) { diff --git a/lib/json.rb b/lib/json.rb index 74e258424..e544fc773 100644 --- a/lib/json.rb +++ b/lib/json.rb @@ -141,18 +141,12 @@ # Option +allow_duplicate_key+ specifies whether duplicate keys in objects # should be ignored or cause an error to be raised: # -# When not specified: -# # The last value is used and a deprecation warning emitted. -# JSON.parse('{"a": 1, "a":2}') => {"a" => 2} -# # warning: detected duplicate keys in JSON object. -# # This will raise an error in json 3.0 unless enabled via `allow_duplicate_key: true` +# When set to +false+, the default: +# JSON.parse('{"a": 1, "a":2}') => duplicate key at line 1 column 1 (JSON::ParserError) # # When set to +true+: # # The last value is used. -# JSON.parse('{"a": 1, "a":2}') => {"a" => 2} -# -# When set to +false+, the future default: -# JSON.parse('{"a": 1, "a":2}') => duplicate key at line 1 column 1 (JSON::ParserError) +# JSON.parse('{"a": 1, "a":2}', allow_duplicate_key: true) => {"a" => 2} # # --- # @@ -360,18 +354,13 @@ # 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 +# With the default, false: # 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. +# With true +# JSON.generate({ foo: 1, "foo" => 2 }, allow_duplicate_key: true) +# # => '{"foo":1,"foo":2}' # # --- # diff --git a/lib/json/common.rb b/lib/json/common.rb index a8a83d9c9..92ee0e4c7 100644 --- a/lib/json/common.rb +++ b/lib/json/common.rb @@ -193,18 +193,13 @@ 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) + def on_mixed_keys_hash(hash) 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 + raise GeneratorError, "detected duplicate key #{key_str.inspect} in #{hash.inspect}" else set[key_str] = true end diff --git a/lib/json/truffle_ruby/generator.rb b/lib/json/truffle_ruby/generator.rb index ee6186fbe..450684a50 100644 --- a/lib/json/truffle_ruby/generator.rb +++ b/lib/json/truffle_ruby/generator.rb @@ -167,6 +167,7 @@ def initialize(opts = nil) @strict = false @max_nesting = 100 @sort_keys = false + @allow_duplicate_key = false configure(opts) if opts end @@ -318,6 +319,7 @@ def configure(opts) self.sort_keys = opts[:sort_keys] if opts.key?(:sort_keys) @depth = opts[:depth] || 0 @buffer_initial_length ||= opts[:buffer_initial_length] + @allow_duplicate_key = !!opts[:allow_duplicate_key] @script_safe = if opts.key?(:script_safe) !!opts[:script_safe] @@ -327,12 +329,6 @@ def configure(opts) false end - if opts.key?(:allow_duplicate_key) - @allow_duplicate_key = !!opts[:allow_duplicate_key] - else - @allow_duplicate_key = nil # nil is deprecation - end - @strict = !!opts[:strict] if opts.key?(:strict) if !opts.key?(:max_nesting) # defaults to 100 @@ -362,10 +358,6 @@ def to_h result[key.to_sym] = instance_variable_get(iv) end - if result[:allow_duplicate_key].nil? - result.delete(:allow_duplicate_key) - end - result end @@ -416,7 +408,7 @@ def generate(obj, anIO = nil) else if key_type && !@allow_duplicate_key && key_type != k.class key_type = nil # stop checking - JSON.send(:on_mixed_keys_hash, obj, !@allow_duplicate_key.nil?) + JSON.send(:on_mixed_keys_hash, obj) end buf << ',' end @@ -569,7 +561,7 @@ def json_transform(state) else if key_type && !state.allow_duplicate_key? && key_type != key.class key_type = nil # stop checking - JSON.send(:on_mixed_keys_hash, self, state.allow_duplicate_key? == false) + JSON.send(:on_mixed_keys_hash, self) end result << delim end diff --git a/test/json/json_generator_test.rb b/test/json/json_generator_test.rb index ab19704b1..94c48a604 100755 --- a/test/json/json_generator_test.rb +++ b/test/json/json_generator_test.rb @@ -329,6 +329,7 @@ def test_falsy_state def test_state_defaults state = JSON::State.new assert_equal({ + :allow_duplicate_key => false, :allow_nan => false, :array_nl => "", :as_json => false, @@ -1090,13 +1091,6 @@ def test_generate_duplicate_keys_allowed 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 diff --git a/test/json/json_parser_test.rb b/test/json/json_parser_test.rb index f75741fd6..f0df848aa 100644 --- a/test/json/json_parser_test.rb +++ b/test/json/json_parser_test.rb @@ -436,35 +436,16 @@ def test_parse_escaped_key def test_parse_duplicate_key expected = {"a" => 2} - expected_sym = {a: 2} assert_equal expected, parse('{"a": 1, "a": 2}', allow_duplicate_key: true) assert_raise(ParserError) { parse('{"a": 1, "a": 2}', allow_duplicate_key: false) } assert_raise(ParserError) { parse('{"a": 1, "a": 2}', allow_duplicate_key: false, symbolize_names: true) } - assert_deprecated_warning(/duplicate key "a"/) do - assert_equal expected, parse('{"a": 1, "a": 2}') - end - assert_deprecated_warning(/duplicate key "a"/) do - assert_equal expected_sym, parse('{"a": 1, "a": 2}', symbolize_names: true) - end - - if RUBY_ENGINE == 'ruby' - assert_deprecated_warning(/#{File.basename(__FILE__)}\:#{__LINE__ + 1}/) do - assert_equal expected, parse('{"a": 1, "a": 2}') - end - end - unless RUBY_ENGINE == 'jruby' assert_raise(ParserError) do fake_key = Object.new JSON.load('{"a": 1, "a": 2}', -> (obj) { obj == "a" ? fake_key : obj }, allow_duplicate_key: false) end - - assert_deprecated_warning(/duplicate key # (obj) { obj == "a" ? fake_key : obj }) - end end end diff --git a/test/json/resumable_parser_test.rb b/test/json/resumable_parser_test.rb index 0b8e13560..289da7547 100644 --- a/test/json/resumable_parser_test.rb +++ b/test/json/resumable_parser_test.rb @@ -689,19 +689,6 @@ def test_parse_error_message assert_equal 0, error.column end - def test_duplicate_key_warning_message - parser = new_parser - parser << (%q({"a":1,"a":2,"pad":") + ("x" * (4 * 1024 * 1024))) - refute parser.parse - - parser << %q(",) - refute parser.parse - - assert_deprecated_warning(/duplicate key "a"/) do - parser.partial_value - end - end - private def assert_parse_error(json, expected_error_message = nil) From e06793e5e24b5c9e4209391cf53780d2719be27d Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Mon, 27 Jul 2026 20:07:20 +0200 Subject: [PATCH 03/18] Make `allow_comments: false` the default --- CHANGES.md | 1 + ext/json/ext/parser/parser.c | 35 +++-------------------------------- java/src/json/ext/Parser.java | 20 ++------------------ lib/json.rb | 8 +++----- test/json/json_parser_test.rb | 8 -------- 5 files changed, 9 insertions(+), 63 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ee3674f7b..77b8b428d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ ### Unreleased +* The `allow_comments` parsing option now default to `false`. * The `allow_duplicate_key` option now defaults to `false`, for both parsing and generating JSON. * Remove `Kernel#j` and `Kernel#jj` diff --git a/ext/json/ext/parser/parser.c b/ext/json/ext/parser/parser.c index 4a6344cb6..54ecb2c89 100644 --- a/ext/json/ext/parser/parser.c +++ b/ext/json/ext/parser/parser.c @@ -403,18 +403,12 @@ typedef struct json_frame_stack_struct { json_frame *ptr; } json_frame_stack; -enum deprecatable_action { - JSON_DEPRECATED = 0, - JSON_IGNORE, - JSON_RAISE, -}; - typedef struct JSON_ParserStruct { VALUE on_load_proc; VALUE decimal_class; ID decimal_method_id; - enum deprecatable_action on_comment; int max_nesting; + bool allow_comments; bool allow_duplicate_key; bool allow_nan; bool allow_trailing_comma; @@ -435,7 +429,6 @@ typedef struct JSON_ParserStateStruct { rvalue_cache name_cache; int in_array; int current_nesting; - unsigned int emitted_deprecations; VALUE parser; } JSON_ParserState; @@ -619,21 +612,6 @@ static void cursor_position(JSON_ParserState *state, long *line_out, long *colum *column_out = column; } -static const unsigned int MAX_DEPRECATIONS = 5; - -static void emit_parse_warning(const char *message, JSON_ParserState *state) -{ - VALUE warning; - if (state->parser) { // line and columns can't be accurate in resumable - warning = rb_utf8_str_new_cstr(message); - } else { - long line, column; - cursor_position(state, &line, &column); - warning = rb_sprintf("%s at line %ld column %ld", message, line, column); - } - rb_funcall(mJSON, rb_intern("deprecation_warning"), 1, warning); -} - #define PARSE_ERROR_FRAGMENT_LEN 32 static VALUE build_parse_error_message(const char *format, JSON_ParserState *state) @@ -772,11 +750,10 @@ static uint32_t unescape_unicode(JSON_ParserState *state, const char *sp, const static const rb_data_type_t JSON_ParserConfig_type; -const char *COMMENT_DEPRECATION_MESSAGE = "Encountered comment in JSON. This will raise an error in json 3.0 unless enabled via `allow_comments: true`"; NOINLINE(static) void json_eat_comments(JSON_ParserState *state, JSON_ParserConfig *config, const char *resume_pos) { - if (config->on_comment == JSON_RAISE) { + if (!config->allow_comments) { raise_syntax_error("unexpected token %s", state); } @@ -826,11 +803,6 @@ json_eat_comments(JSON_ParserState *state, JSON_ParserConfig *config, const char raise_parse_error_at("unexpected token %s", state, eos(state) ? rewind_pos : start, eos(state)); break; } - - if (config->on_comment == JSON_DEPRECATED && state->emitted_deprecations < MAX_DEPRECATIONS) { - state->emitted_deprecations++; - emit_parse_warning(COMMENT_DEPRECATION_MESSAGE, state); - } } ALWAYS_INLINE(static) void @@ -2007,7 +1979,7 @@ static int parser_config_init_i(VALUE key, VALUE val, VALUE data) if (key == sym_max_nesting) { config->max_nesting = RTEST(val) ? FIX2INT(val) : 0; } else if (key == sym_allow_nan) { config->allow_nan = RTEST(val); } else if (key == sym_allow_trailing_comma) { config->allow_trailing_comma = RTEST(val); } - else if (key == sym_allow_comments) { config->on_comment = RTEST(val) ? JSON_IGNORE : JSON_RAISE; } + else if (key == sym_allow_comments) { config->allow_comments = RTEST(val); } else if (key == sym_allow_control_characters) { config->allow_control_characters = RTEST(val); } else if (key == sym_allow_invalid_escape) { config->allow_invalid_escape = RTEST(val); } else if (key == sym_symbolize_names) { config->symbolize_names = RTEST(val); } @@ -2641,7 +2613,6 @@ static VALUE cResumableParser_clear(VALUE self) parser->state.name_cache.length = 0; parser->state.current_nesting = 0; parser->state.in_array = 1; - parser->state.emitted_deprecations = 0; parser->state.start = parser->state.cursor = parser->state.end = NULL; return self; } diff --git a/java/src/json/ext/Parser.java b/java/src/json/ext/Parser.java index 6d5c18e06..b2be38d15 100644 --- a/java/src/json/ext/Parser.java +++ b/java/src/json/ext/Parser.java @@ -46,7 +46,6 @@ public class Parser extends RubyObject { private boolean allowNaN; private boolean allowTrailingComma; private boolean allowComments; - private boolean deprecateComments; private boolean allowControlCharacters; private boolean allowInvalidEscape; private boolean allowDuplicateKey; @@ -96,14 +95,7 @@ public IRubyObject initialize(ThreadContext context, IRubyObject options) { OptionsReader opts = new OptionsReader(context, options); this.maxNesting = opts.getInt("max_nesting", DEFAULT_MAX_NESTING); this.allowNaN = opts.getBool("allow_nan", false); - if (opts.hasKey("allow_comments")) { - this.allowComments = opts.getBool("allow_comments", false); - this.deprecateComments = false; - } else { - this.allowComments = true; - this.deprecateComments = true; - } - + this.allowComments = opts.getBool("allow_comments", false); this.allowControlCharacters = opts.getBool("allow_control_characters", false); this.allowInvalidEscape = opts.getBool("allow_invalid_escape", false); this.allowTrailingComma = opts.getBool("allow_trailing_comma", false); @@ -843,15 +835,7 @@ private void eatWhitespace() { private void eatComments() { if (!config.allowComments) { - if (config.deprecateComments) { - if (emittedDeprecations < MAX_DEPRECATIONS) { - emittedDeprecations++; - context.runtime.getWarnings().warning( - "Encountered comment in JSON. This will raise an error in json 3.0 unless enabled via `allow_comments: true`"); - } - } else { - throw unexpectedToken(cursor, end); - } + throw unexpectedToken(cursor, end); } int start = cursor; diff --git a/lib/json.rb b/lib/json.rb index e544fc773..90b4b3c91 100644 --- a/lib/json.rb +++ b/lib/json.rb @@ -184,13 +184,11 @@ # JavaScript style comments (either // comment or /* comment */); # defaults to +false+. # -# When not specified, a deprecation warning is emitted if a comment is encountered. +# When set to +false+, the default: +# JSON.parse('/* comment */ {"a": 1, "a":2}') # unexpected character: '/' at line 1 column 1 (JSON::ParserError) # # When set to +true+, comments are ignored: -# JSON.parse('/* comment */ {"a": 1, "a":2}') # => {"a" => 2} -# -# When set to +false+, the future default: -# JSON.parse('/* comment */ {"a": 1, "a":2}') # unexpected character: '/' at line 1 column 1 (JSON::ParserError) +# JSON.parse('/* comment */ {"a": 1, "a":2} // more comment') # => {"a" => 2} # # --- # diff --git a/test/json/json_parser_test.rb b/test/json/json_parser_test.rb index f0df848aa..1f257e71d 100644 --- a/test/json/json_parser_test.rb +++ b/test/json/json_parser_test.rb @@ -543,16 +543,8 @@ def test_parse_comments assert_raise(ParserError) { parse('{} /*/', allow_comments: true) } assert_raise(ParserError) { parse('{} /x wrong comment', allow_comments: true) } assert_raise(ParserError) { parse('{} /', allow_comments: true) } - end - - def test_parse_comments_deprecation assert_equal({}, parse('/**/ {}', allow_comments: true)) assert_raise(ParserError) { parse('/**/ {}', allow_comments: false) } - if RUBY_ENGINE == 'ruby' - assert_deprecated_warning(/Encountered comment in JSON/) do - parse('/**/ {}') - end - end end def test_nesting From 52b97f620d889d73d2eb042ab7d54c3aaddb4281 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Mon, 27 Jul 2026 20:28:40 +0200 Subject: [PATCH 04/18] Remove `default_options` singleton accessors --- CHANGES.md | 5 +- lib/json/common.rb | 108 +++++------------------- test/json/json_addition_test.rb | 6 -- test/json/json_common_interface_test.rb | 12 --- 4 files changed, 25 insertions(+), 106 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 77b8b428d..da1ef1ef1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,7 +4,10 @@ * The `allow_comments` parsing option now default to `false`. * The `allow_duplicate_key` option now defaults to `false`, for both parsing and generating JSON. -* Remove `Kernel#j` and `Kernel#jj` +* Removed `Kernel#j` and `Kernel#jj` +* Removed `JSON.load_default_options`. +* Removed `JSON.unsafe_load_default_options`. +* Removed `JSON.dump_default_options`. ### 2026-07-31 (2.21.2) diff --git a/lib/json/common.rb b/lib/json/common.rb index 92ee0e4c7..44be124e0 100644 --- a/lib/json/common.rb +++ b/lib/json/common.rb @@ -64,7 +64,6 @@ def create_additions_proc(opts) when String opts[:match_string]&.each do |pattern, klass| if match = pattern.match(object) - create_additions_warning if create_additions.nil? object = klass.json_create(object) break end @@ -79,7 +78,6 @@ def create_additions_proc(opts) end if klass.respond_to?(:json_creatable?) ? klass.json_creatable? : klass.respond_to?(:json_create) - create_additions_warning if create_additions.nil? object = klass.json_create(object) end end @@ -91,12 +89,6 @@ def create_additions_proc(opts) opts end - - def create_additions_warning - JSON.deprecation_warning "JSON.load implicit support for `create_additions: true` is deprecated " \ - "and will be removed in 3.0, use JSON.unsafe_load or explicitly " \ - "pass `create_additions: true`" - end end end @@ -205,27 +197,6 @@ def on_mixed_keys_hash(hash) end end end - - def deprecated_singleton_attr_accessor(*attrs) - args = RUBY_VERSION >= "3.0" ? ", category: :deprecated" : "" - attrs.each do |attr| - singleton_class.class_eval <<~RUBY - def #{attr} - warn "JSON.#{attr} is deprecated and will be removed in json 3.0.0", uplevel: 1 #{args} - @#{attr} - end - - def #{attr}=(val) - warn "JSON.#{attr}= is deprecated and will be removed in json 3.0.0", uplevel: 1 #{args} - @#{attr} = val - end - - def _#{attr} - @#{attr} - end - RUBY - end - end end # Sets create identifier, which is used to decide if the _json_create_ @@ -531,30 +502,6 @@ def pretty_generate(obj, opts = nil) State.generate(obj, options, nil) end - # Sets or returns default options for the JSON.unsafe_load method. - # Initially: - # opts = JSON.load_default_options - # opts # => {:max_nesting=>false, :allow_nan=>true, :allow_blank=>true, :create_additions=>true} - deprecated_singleton_attr_accessor :unsafe_load_default_options - - @unsafe_load_default_options = { - :max_nesting => false, - :allow_nan => true, - :allow_blank => true, - :create_additions => true, - } - - # Sets or returns default options for the JSON.load method. - # Initially: - # opts = JSON.load_default_options - # opts # => {:max_nesting=>false, :allow_nan=>true, :allow_blank=>true, :create_additions=>true} - deprecated_singleton_attr_accessor :load_default_options - - @load_default_options = { - :allow_nan => true, - :allow_blank => true, - :create_additions => nil, - } # :call-seq: # JSON.unsafe_load(source, options = {}) -> object # JSON.unsafe_load(source, proc = nil, options = {}) -> object @@ -581,7 +528,6 @@ def pretty_generate(obj, opts = nil) # See details below. # - Argument +opts+, if given, contains a \Hash of options for the parsing. # See {Parsing Options}[#module-JSON-label-Parsing+Options]. - # The default options can be changed via method JSON.unsafe_load_default_options=. # # --- # @@ -687,17 +633,18 @@ def pretty_generate(obj, opts = nil) # @attributes={"type"=>"Admin", "password"=>"0wn3d"}>} # def unsafe_load(source, proc = nil, options = nil) - opts = if options.nil? - if proc && proc.is_a?(Hash) - options, proc = proc, nil - options - else - _unsafe_load_default_options - end - else - _unsafe_load_default_options.merge(options) + opts = { + max_nesting: false, + allow_nan: true, + allow_blank: true, + create_additions: true, + } + if options.nil? && proc && proc.is_a?(Hash) + options, proc = proc, nil end + opts.merge!(options) unless options.nil? + unless source.is_a?(String) if source.respond_to? :to_str source = source.to_str @@ -752,7 +699,6 @@ def unsafe_load(source, proc = nil, options = nil) # See details below. # - Argument +opts+, if given, contains a \Hash of options for the parsing. # See {Parsing Options}[#module-JSON-label-Parsing+Options]. - # The default options can be changed via method JSON.load_default_options=. # # --- # @@ -863,16 +809,12 @@ def load(source, proc = nil, options = nil) proc = nil end - opts = if options.nil? - if proc && proc.is_a?(Hash) - options, proc = proc, nil - options - else - _load_default_options - end - else - _load_default_options.merge(options) - end + opts = { + allow_nan: true, + allow_blank: true, + } + + opts.merge!(options) unless options.nil? unless source.is_a?(String) if source.respond_to? :to_str @@ -896,16 +838,6 @@ def load(source, proc = nil, options = nil) parse(source, opts) end - # Sets or returns the default options for the JSON.dump method. - # Initially: - # opts = JSON.dump_default_options - # opts # => {:max_nesting=>false, :allow_nan=>true} - deprecated_singleton_attr_accessor :dump_default_options - @dump_default_options = { - :max_nesting => false, - :allow_nan => true, - } - # :call-seq: # JSON.dump(obj, io = nil, limit = nil) # @@ -954,9 +886,11 @@ def dump(obj, anIO = nil, limit = nil, kwargs = nil) end end - opts = JSON._dump_default_options - opts = opts.merge(:max_nesting => limit) if limit - opts = opts.merge(kwargs) if kwargs + opts = { + allow_nan: true, + max_nesting: limit, + } + opts.merge!(kwargs) if kwargs begin State.generate(obj, opts, anIO) diff --git a/test/json/json_addition_test.rb b/test/json/json_addition_test.rb index 4d8d18687..fedd1f1d9 100644 --- a/test/json/json_addition_test.rb +++ b/test/json/json_addition_test.rb @@ -150,12 +150,6 @@ def test_core assert_equal(/foo/i, JSON(JSON(/foo/i), :create_additions => true)) end - def test_deprecated_load_create_additions - assert_deprecated_warning(/use JSON\.unsafe_load/) do - JSON.load(JSON.dump(Time.now)) - end - end - def test_utc_datetime now = Time.now d = DateTime.parse(now.to_s) # usual case diff --git a/test/json/json_common_interface_test.rb b/test/json/json_common_interface_test.rb index 37568b556..72b8f390a 100644 --- a/test/json/json_common_interface_test.rb +++ b/test/json/json_common_interface_test.rb @@ -271,12 +271,6 @@ def test_dump_in_io assert_equal JSON.dump(big_object), io.string end - def test_dump_should_modify_defaults - max_nesting = JSON._dump_default_options[:max_nesting] - dump([], StringIO.new, 10) - assert_equal max_nesting, JSON._dump_default_options[:max_nesting] - end - def test_JSON assert_equal @json, JSON(@hash) assert_equal @json, JSON(@hash_with_method_missing) @@ -315,12 +309,6 @@ def test_load_file_with_bad_default_external_encoding end end - def test_deprecated_dump_default_options - assert_deprecated_warning(/dump_default_options/) do - JSON.dump_default_options - end - end - private def with_external_encoding(encoding) From 51a45b88a3c51a2e307723ed974b67edee9940c3 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Mon, 27 Jul 2026 20:34:31 +0200 Subject: [PATCH 05/18] Remove `JSON::State#[]` and `JSON::State#[]=` --- CHANGES.md | 3 ++- java/src/json/ext/GeneratorState.java | 24 ----------------- lib/json/common.rb | 17 ------------ lib/json/ext/generator/state.rb | 27 ------------------- lib/json/truffle_ruby/generator.rb | 22 ---------------- test/json/json_generator_test.rb | 37 --------------------------- 6 files changed, 2 insertions(+), 128 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index da1ef1ef1..48d96a615 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,10 +4,11 @@ * The `allow_comments` parsing option now default to `false`. * The `allow_duplicate_key` option now defaults to `false`, for both parsing and generating JSON. -* Removed `Kernel#j` and `Kernel#jj` +* Removed `Kernel#j` and `Kernel#jj`. * Removed `JSON.load_default_options`. * Removed `JSON.unsafe_load_default_options`. * Removed `JSON.dump_default_options`. +* Removed `JSON::State#[]` and `JSON::State#[]=`. ### 2026-07-31 (2.21.2) diff --git a/java/src/json/ext/GeneratorState.java b/java/src/json/ext/GeneratorState.java index b5b28d2be..1086bf6d3 100644 --- a/java/src/json/ext/GeneratorState.java +++ b/java/src/json/ext/GeneratorState.java @@ -270,30 +270,6 @@ public IRubyObject generate(ThreadContext context, IRubyObject obj) { return generate(context, obj, context.nil); } - @JRubyMethod(name="[]") - public IRubyObject op_aref(ThreadContext context, IRubyObject vName) { - String name = vName.asJavaString(); - if (getMetaClass().isMethodBound(name, true)) { - return send(context, vName, Block.NULL_BLOCK); - } else { - IRubyObject value = getInstanceVariables().getInstanceVariable("@" + name); - return value == null ? context.nil : value; - } - } - - @JRubyMethod(name="[]=") - public IRubyObject op_aset(ThreadContext context, IRubyObject vName, IRubyObject value) { - checkFrozen(); - String name = vName.asJavaString(); - String nameWriter = name + "="; - if (getMetaClass().isMethodBound(nameWriter, true)) { - return send(context, context.runtime.newString(nameWriter), value, Block.NULL_BLOCK); - } else { - getInstanceVariables().setInstanceVariable("@" + name, value); - } - return context.nil; - } - public ByteList getIndent() { return indent; } diff --git a/lib/json/common.rb b/lib/json/common.rb index 44be124e0..5fea1c532 100644 --- a/lib/json/common.rb +++ b/lib/json/common.rb @@ -93,23 +93,6 @@ def create_additions_proc(opts) end class << self - def deprecation_warning(message, uplevel = 3) # :nodoc: - gem_root = File.expand_path("..", __dir__) + "/" - caller_locations(uplevel, 10).each do |frame| - if frame.path.nil? || frame.path.start_with?(gem_root) || frame.path.end_with?("/truffle/cext_ruby.rb", ".c") - uplevel += 1 - else - break - end - end - - if RUBY_VERSION >= "3.0" - warn(message, uplevel: uplevel, category: :deprecated) - else - warn(message, uplevel: uplevel) - end - end - # :call-seq: # JSON[object] -> new_array or new_string # diff --git a/lib/json/ext/generator/state.rb b/lib/json/ext/generator/state.rb index 3c1d2fb94..a9578b9bd 100644 --- a/lib/json/ext/generator/state.rb +++ b/lib/json/ext/generator/state.rb @@ -71,33 +71,6 @@ def to_h end alias_method :to_hash, :to_h - - # call-seq: [](name) - # - # Returns the value returned by method +name+. - def [](name) - ::JSON.deprecation_warning("JSON::State#[] is deprecated and will be removed in json 3.0.0") - - if respond_to?(name) - __send__(name) - else - instance_variable_get("@#{name}") if - instance_variables.include?("@#{name}".to_sym) # avoid warning - end - end - - # call-seq: []=(name, value) - # - # Sets the attribute name to value. - def []=(name, value) - ::JSON.deprecation_warning("JSON::State#[]= is deprecated and will be removed in json 3.0.0") - - if respond_to?(name_writer = "#{name}=") - __send__ name_writer, value - else - instance_variable_set "@#{name}", value - end - end end end end diff --git a/lib/json/truffle_ruby/generator.rb b/lib/json/truffle_ruby/generator.rb index 450684a50..495167be2 100644 --- a/lib/json/truffle_ruby/generator.rb +++ b/lib/json/truffle_ruby/generator.rb @@ -475,28 +475,6 @@ def generate(obj, anIO = nil) end buf << '"' end - - # Return the value returned by method +name+. - def [](name) - ::JSON.deprecation_warning("JSON::State#[] is deprecated and will be removed in json 3.0.0") - - if respond_to?(name) - __send__(name) - else - instance_variable_get("@#{name}") if - instance_variables.include?("@#{name}".to_sym) # avoid warning - end - end - - def []=(name, value) - ::JSON.deprecation_warning("JSON::State#[]= is deprecated and will be removed in json 3.0.0") - - if respond_to?(name_writer = "#{name}=") - __send__ name_writer, value - else - instance_variable_set "@#{name}", value - end - end end module GeneratorMethods diff --git a/test/json/json_generator_test.rb b/test/json/json_generator_test.rb index 94c48a604..937f71651 100755 --- a/test/json/json_generator_test.rb +++ b/test/json/json_generator_test.rb @@ -287,10 +287,6 @@ def test_states json = generate({1=>2}, nil) assert_equal('{"1":2}', json) s = JSON.state.new - assert s.check_circular? - assert_deprecated_warning(/JSON::State/) do - assert s[:check_circular?] - end h = { 1=>2 } h[3] = h assert_raise(JSON::NestingError) { generate(h) } @@ -299,10 +295,6 @@ def test_states a = [ 1, 2 ] a << a assert_raise(JSON::NestingError) { generate(a, s) } - assert s.check_circular? - assert_deprecated_warning(/JSON::State/) do - assert s[:check_circular?] - end end def test_falsy_state @@ -578,35 +570,6 @@ def to_s bignum.class.define_method(:to_s, original_to_s) if original_to_s end - def test_hash_likeness_set_symbol - assert_deprecated_warning(/JSON::State/) do - state = JSON.state.new - assert_equal nil, state[:foo] - assert_equal nil.class, state[:foo].class - assert_equal nil, state['foo'] - state[:foo] = :bar - assert_equal :bar, state[:foo] - assert_equal :bar, state['foo'] - state_hash = state.to_hash - assert_kind_of Hash, state_hash - assert_equal :bar, state_hash[:foo] - end - end - - def test_hash_likeness_set_string - assert_deprecated_warning(/JSON::State/) do - state = JSON.state.new - assert_equal nil, state[:foo] - assert_equal nil, state['foo'] - state['foo'] = :bar - assert_equal :bar, state[:foo] - assert_equal :bar, state['foo'] - state_hash = state.to_hash - assert_kind_of Hash, state_hash - assert_equal :bar, state_hash[:foo] - end - end - def test_json_state_to_h_roundtrip state = JSON.state.new assert_equal state.to_h, JSON.state.new(state.to_h).to_h From 091df28cacf5072bd8d1363b1f8573a44827ecc1 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Mon, 27 Jul 2026 20:36:55 +0200 Subject: [PATCH 06/18] Remove deprecated JSON.dump aliases --- CHANGES.md | 4 ++++ lib/json/common.rb | 42 ------------------------------------------ 2 files changed, 4 insertions(+), 42 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 48d96a615..483e8983c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,10 @@ * Removed `JSON.unsafe_load_default_options`. * Removed `JSON.dump_default_options`. * Removed `JSON::State#[]` and `JSON::State#[]=`. +* Removed `JSON.unparse`. +* Removed `JSON.fast_unparse`. +* Removed `JSON.pretty_unparse`. +* Removed `JSON.restore`. ### 2026-07-31 (2.21.2) diff --git a/lib/json/common.rb b/lib/json/common.rb index 5fea1c532..5a1b4be23 100644 --- a/lib/json/common.rb +++ b/lib/json/common.rb @@ -883,48 +883,6 @@ def dump(obj, anIO = nil, limit = nil, kwargs = nil) end # :stopdoc: - # All these were meant to be deprecated circa 2009, but were just set as undocumented - # so usage still exist in the wild. - def unparse(...) - if RUBY_VERSION >= "3.0" - warn "JSON.unparse is deprecated and will be removed in json 3.0.0, just use JSON.generate", uplevel: 1, category: :deprecated - else - warn "JSON.unparse is deprecated and will be removed in json 3.0.0, just use JSON.generate", uplevel: 1 - end - generate(...) - end - module_function :unparse - - def fast_unparse(...) - if RUBY_VERSION >= "3.0" - warn "JSON.fast_unparse is deprecated and will be removed in json 3.0.0, just use JSON.generate", uplevel: 1, category: :deprecated - else - warn "JSON.fast_unparse is deprecated and will be removed in json 3.0.0, just use JSON.generate", uplevel: 1 - end - generate(...) - end - module_function :fast_unparse - - def pretty_unparse(...) - if RUBY_VERSION >= "3.0" - warn "JSON.pretty_unparse is deprecated and will be removed in json 3.0.0, just use JSON.pretty_generate", uplevel: 1, category: :deprecated - else - warn "JSON.pretty_unparse is deprecated and will be removed in json 3.0.0, just use JSON.pretty_generate", uplevel: 1 - end - pretty_generate(...) - end - module_function :fast_unparse - - def restore(...) - if RUBY_VERSION >= "3.0" - warn "JSON.restore is deprecated and will be removed in json 3.0.0, just use JSON.load", uplevel: 1, category: :deprecated - else - warn "JSON.restore is deprecated and will be removed in json 3.0.0, just use JSON.load", uplevel: 1 - end - load(...) - end - module_function :restore - class << self private From 51bb2882622cd9d3bb63e958011318baf1c765c2 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Mon, 27 Jul 2026 20:38:23 +0200 Subject: [PATCH 07/18] Remove PRETTY_STATE_PROTOTYPE --- CHANGES.md | 1 + lib/json/common.rb | 20 -------------------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 483e8983c..a2c5e137c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,6 +13,7 @@ * Removed `JSON.fast_unparse`. * Removed `JSON.pretty_unparse`. * Removed `JSON.restore`. +* Removed `JSON::PRETTY_STATE_PROTOTYPE`. ### 2026-07-31 (2.21.2) diff --git a/lib/json/common.rb b/lib/json/common.rb index 5a1b4be23..a030f0464 100644 --- a/lib/json/common.rb +++ b/lib/json/common.rb @@ -882,26 +882,6 @@ def dump(obj, anIO = nil, limit = nil, kwargs = nil) end end - # :stopdoc: - class << self - private - - def const_missing(const_name) - case const_name - when :PRETTY_STATE_PROTOTYPE - if RUBY_VERSION >= "3.0" - warn "JSON::PRETTY_STATE_PROTOTYPE is deprecated and will be removed in json 3.0.0, just use JSON.pretty_generate", uplevel: 1, category: :deprecated - else - warn "JSON::PRETTY_STATE_PROTOTYPE is deprecated and will be removed in json 3.0.0, just use JSON.pretty_generate", uplevel: 1 - end - state.new(PRETTY_GENERATE_OPTIONS) - else - super - end - end - end - # :startdoc: - # JSON::Coder holds a parser and generator configuration. # # module MyApp From bf6b42bede91dc9fc40c75f4cd4f3ed8142c5e05 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Mon, 27 Jul 2026 20:56:36 +0200 Subject: [PATCH 08/18] Remove `create_additions` option --- CHANGES.md | 1 + README.md | 74 +------- benchmark/standalone.rb | 2 +- lib/json.rb | 240 ------------------------ lib/json/add/bigdecimal.rb | 58 ------ lib/json/add/complex.rb | 51 ----- lib/json/add/core.rb | 13 -- lib/json/add/date.rb | 54 ------ lib/json/add/date_time.rb | 67 ------- lib/json/add/exception.rb | 49 ----- lib/json/add/ostruct.rb | 54 ------ lib/json/add/range.rb | 54 ------ lib/json/add/rational.rb | 49 ----- lib/json/add/regexp.rb | 48 ----- lib/json/add/set.rb | 48 ----- lib/json/add/string.rb | 35 ---- lib/json/add/struct.rb | 52 ----- lib/json/add/symbol.rb | 52 ----- lib/json/add/time.rb | 52 ----- lib/json/common.rb | 72 +------ lib/json/generic_object.rb | 7 - test/json/json_addition_test.rb | 186 ------------------ test/json/json_common_interface_test.rb | 8 - test/json/json_generic_object_test.rb | 40 +--- test/json/json_parser_test.rb | 29 --- test/json/json_string_matching_test.rb | 38 ---- 26 files changed, 10 insertions(+), 1423 deletions(-) delete mode 100644 lib/json/add/bigdecimal.rb delete mode 100644 lib/json/add/complex.rb delete mode 100644 lib/json/add/core.rb delete mode 100644 lib/json/add/date.rb delete mode 100644 lib/json/add/date_time.rb delete mode 100644 lib/json/add/exception.rb delete mode 100644 lib/json/add/ostruct.rb delete mode 100644 lib/json/add/range.rb delete mode 100644 lib/json/add/rational.rb delete mode 100644 lib/json/add/regexp.rb delete mode 100644 lib/json/add/set.rb delete mode 100644 lib/json/add/string.rb delete mode 100644 lib/json/add/struct.rb delete mode 100644 lib/json/add/symbol.rb delete mode 100644 lib/json/add/time.rb delete mode 100644 test/json/json_addition_test.rb delete mode 100644 test/json/json_string_matching_test.rb diff --git a/CHANGES.md b/CHANGES.md index a2c5e137c..382428b19 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,7 @@ * Removed `JSON.pretty_unparse`. * Removed `JSON.restore`. * Removed `JSON::PRETTY_STATE_PROTOTYPE`. +* Removed the insecure `create_additions` option. ### 2026-07-31 (2.21.2) diff --git a/README.md b/README.md index 5e6ffe48f..79a8c3f3e 100644 --- a/README.md +++ b/README.md @@ -143,79 +143,7 @@ posts_json.map! { |post_json| JSON::Fragment.new(post_json) } JSON.generate({ posts: posts_json, count: posts_json.count }) ``` -## Round-tripping arbitrary types - -> [!CAUTION] -> You should never use `JSON.unsafe_load` nor `JSON.parse(str, create_additions: true)` to parse untrusted user input, -> as it can lead to remote code execution vulnerabilities. - -To create a JSON document from a ruby data structure, you can call -`JSON.generate` like that: - -```ruby -json = JSON.generate [1, 2, {"a"=>3.141}, false, true, nil, 4..10] -# => "[1,2,{\"a\":3.141},false,true,null,\"4..10\"]" -``` - -To get back a ruby data structure from a JSON document, you have to call -JSON.parse on it: - -```ruby -JSON.parse json -# => [1, 2, {"a"=>3.141}, false, true, nil, "4..10"] -``` - -Note, that the range from the original data structure is a simple -string now. The reason for this is, that JSON doesn't support ranges -or arbitrary classes. In this case the json library falls back to call -`Object#to_json`, which is the same as `#to_s.to_json`. - -It's possible to add JSON support serialization to arbitrary classes by -simply implementing a more specialized version of the `#to_json method`, that -should return a JSON object (a hash converted to JSON with `#to_json`) like -this (don't forget the `*a` for all the arguments): - -```ruby -class Range - def to_json(*a) - { - 'json_class' => self.class.name, # = 'Range' - 'data' => [ first, last, exclude_end? ] - }.to_json(*a) - end -end -``` - -The hash key `json_class` is the class, that will be asked to deserialise the -JSON representation later. In this case it's `Range`, but any namespace of -the form `A::B` or `::A::B` will do. All other keys are arbitrary and can be -used to store the necessary data to configure the object to be deserialised. - -If the key `json_class` is found in a JSON object, the JSON parser checks -if the given class responds to the `json_create` class method. If so, it is -called with the JSON object converted to a Ruby hash. So a range can -be deserialised by implementing `Range.json_create` like this: - -```ruby -class Range - def self.json_create(o) - new(*o['data']) - end -end -``` - -Now it possible to serialise/deserialise ranges as well: - -```ruby -json = JSON.generate [1, 2, {"a"=>3.141}, false, true, nil, 4..10] -# => "[1,2,{\"a\":3.141},false,true,null,{\"json_class\":\"Range\",\"data\":[4,10,false]}]" -JSON.parse json -# => [1, 2, {"a"=>3.141}, false, true, nil, 4..10] -json = JSON.generate [1, 2, {"a"=>3.141}, false, true, nil, 4..10] -# => "[1,2,{\"a\":3.141},false,true,null,{\"json_class\":\"Range\",\"data\":[4,10,false]}]" -JSON.unsafe_load json -# => [1, 2, {"a"=>3.141}, false, true, nil, 4..10] -``` +## Pretty Printing `JSON.generate` always creates the shortest possible string representation of a ruby data structure in one line. This is good for data storage or network diff --git a/benchmark/standalone.rb b/benchmark/standalone.rb index deeb45dce..ebe759c5f 100644 --- a/benchmark/standalone.rb +++ b/benchmark/standalone.rb @@ -32,7 +32,7 @@ JSON.dump(obj) end else - x.report('JSON.load(str)') do # max_nesting: false, allow_nan: true, allow_blank: true, create_additions: true + x.report('JSON.load(str)') do # max_nesting: false, allow_nan: true, allow_blank: true JSON.load(str) end end diff --git a/lib/json.rb b/lib/json.rb index 90b4b3c91..9c77544f1 100644 --- a/lib/json.rb +++ b/lib/json.rb @@ -259,11 +259,6 @@ # ruby = JSON.parse(source, {array_class: Set}) # ruby # => # # -# --- -# -# Option +create_additions+ (boolean) specifies whether to use \JSON additions in parsing. -# See {\JSON Additions}[#module-JSON-label-JSON+Additions]. -# # === Generating \JSON # # To generate a Ruby \String containing \JSON data, @@ -450,241 +445,6 @@ # } # } # -# == \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) -# json = JSON.generate(ruby0) -# json # => '0..2"' -# ruby1 = JSON.parse(json) -# ruby1 # => '0..2' -# ruby1.class # => String -# -# You can use \JSON _additions_ to preserve the original object. -# The addition is an extension of a ruby class, so that: -# - \JSON.generate stores more information in the \JSON string. -# - \JSON.parse, called with option +create_additions+, -# uses that information to create a proper Ruby object. -# -# This example shows a \Range being generated into \JSON -# and parsed back into Ruby, both without and with -# the addition for \Range: -# ruby = Range.new(0, 2) -# # This passage does not use the addition for Range. -# json0 = JSON.generate(ruby) -# ruby0 = JSON.parse(json0) -# # This passage uses the addition for Range. -# require 'json/add/range' -# json1 = JSON.generate(ruby) -# ruby1 = JSON.parse(json1, create_additions: true) -# # Make a nice display. -# display = <<~EOT -# Generated JSON: -# Without addition: #{json0} (#{json0.class}) -# With addition: #{json1} (#{json1.class}) -# Parsed JSON: -# Without addition: #{ruby0.inspect} (#{ruby0.class}) -# With addition: #{ruby1.inspect} (#{ruby1.class}) -# EOT -# puts display -# -# This output shows the different results: -# Generated JSON: -# Without addition: "0..2" (String) -# With addition: {"json_class":"Range","a":[0,2,false]} (String) -# Parsed JSON: -# Without addition: "0..2" (String) -# With addition: 0..2 (Range) -# -# The \JSON module includes additions for certain classes. -# You can also craft custom additions. -# See {Custom \JSON Additions}[#module-JSON-label-Custom+JSON+Additions]. -# -# === Built-in Additions -# -# The \JSON module includes additions for certain classes. -# To use an addition, +require+ its source: -# - BigDecimal: require 'json/add/bigdecimal' -# - Complex: require 'json/add/complex' -# - Date: require 'json/add/date' -# - DateTime: require 'json/add/date_time' -# - Exception: require 'json/add/exception' -# - OpenStruct: require 'json/add/ostruct' -# - Range: require 'json/add/range' -# - Rational: require 'json/add/rational' -# - Regexp: require 'json/add/regexp' -# - Set: require 'json/add/set' -# - Struct: require 'json/add/struct' -# - Symbol: require 'json/add/symbol' -# - Time: require 'json/add/time' -# -# To reduce punctuation clutter, the examples below -# show the generated \JSON via +puts+, rather than the usual +inspect+, -# -# \BigDecimal: -# require 'json/add/bigdecimal' -# ruby0 = BigDecimal(0) # 0.0 -# json = JSON.generate(ruby0) # {"json_class":"BigDecimal","b":"27:0.0"} -# ruby1 = JSON.parse(json, create_additions: true) # 0.0 -# ruby1.class # => BigDecimal -# -# \Complex: -# require 'json/add/complex' -# ruby0 = Complex(1+0i) # 1+0i -# json = JSON.generate(ruby0) # {"json_class":"Complex","r":1,"i":0} -# ruby1 = JSON.parse(json, create_additions: true) # 1+0i -# ruby1.class # Complex -# -# \Date: -# require 'json/add/date' -# ruby0 = Date.today # 2020-05-02 -# json = JSON.generate(ruby0) # {"json_class":"Date","y":2020,"m":5,"d":2,"sg":2299161.0} -# ruby1 = JSON.parse(json, create_additions: true) # 2020-05-02 -# ruby1.class # Date -# -# \DateTime: -# require 'json/add/date_time' -# ruby0 = DateTime.now # 2020-05-02T10:38:13-05:00 -# json = JSON.generate(ruby0) # {"json_class":"DateTime","y":2020,"m":5,"d":2,"H":10,"M":38,"S":13,"of":"-5/24","sg":2299161.0} -# ruby1 = JSON.parse(json, create_additions: true) # 2020-05-02T10:38:13-05:00 -# ruby1.class # DateTime -# -# \Exception (and its subclasses including \RuntimeError): -# require 'json/add/exception' -# ruby0 = Exception.new('A message') # A message -# json = JSON.generate(ruby0) # {"json_class":"Exception","m":"A message","b":null} -# ruby1 = JSON.parse(json, create_additions: true) # A message -# ruby1.class # Exception -# ruby0 = RuntimeError.new('Another message') # Another message -# json = JSON.generate(ruby0) # {"json_class":"RuntimeError","m":"Another message","b":null} -# ruby1 = JSON.parse(json, create_additions: true) # Another message -# ruby1.class # RuntimeError -# -# \OpenStruct: -# require 'json/add/ostruct' -# ruby0 = OpenStruct.new(name: 'Matz', language: 'Ruby') # # -# json = JSON.generate(ruby0) # {"json_class":"OpenStruct","t":{"name":"Matz","language":"Ruby"}} -# ruby1 = JSON.parse(json, create_additions: true) # # -# ruby1.class # OpenStruct -# -# \Range: -# require 'json/add/range' -# ruby0 = Range.new(0, 2) # 0..2 -# json = JSON.generate(ruby0) # {"json_class":"Range","a":[0,2,false]} -# ruby1 = JSON.parse(json, create_additions: true) # 0..2 -# ruby1.class # Range -# -# \Rational: -# require 'json/add/rational' -# ruby0 = Rational(1, 3) # 1/3 -# json = JSON.generate(ruby0) # {"json_class":"Rational","n":1,"d":3} -# ruby1 = JSON.parse(json, create_additions: true) # 1/3 -# ruby1.class # Rational -# -# \Regexp: -# require 'json/add/regexp' -# ruby0 = Regexp.new('foo') # (?-mix:foo) -# json = JSON.generate(ruby0) # {"json_class":"Regexp","o":0,"s":"foo"} -# ruby1 = JSON.parse(json, create_additions: true) # (?-mix:foo) -# ruby1.class # Regexp -# -# \Set: -# require 'json/add/set' -# ruby0 = Set.new([0, 1, 2]) # # -# json = JSON.generate(ruby0) # {"json_class":"Set","a":[0,1,2]} -# ruby1 = JSON.parse(json, create_additions: true) # # -# ruby1.class # Set -# -# \Struct: -# require 'json/add/struct' -# Customer = Struct.new(:name, :address) # Customer -# ruby0 = Customer.new("Dave", "123 Main") # # -# json = JSON.generate(ruby0) # {"json_class":"Customer","v":["Dave","123 Main"]} -# ruby1 = JSON.parse(json, create_additions: true) # # -# ruby1.class # Customer -# -# \Symbol: -# require 'json/add/symbol' -# ruby0 = :foo # foo -# json = JSON.generate(ruby0) # {"json_class":"Symbol","s":"foo"} -# ruby1 = JSON.parse(json, create_additions: true) # foo -# ruby1.class # Symbol -# -# \Time: -# require 'json/add/time' -# ruby0 = Time.now # 2020-05-02 11:28:26 -0500 -# json = JSON.generate(ruby0) # {"json_class":"Time","s":1588436906,"n":840560000} -# ruby1 = JSON.parse(json, create_additions: true) # 2020-05-02 11:28:26 -0500 -# ruby1.class # Time -# -# -# === Custom \JSON Additions -# -# In addition to the \JSON additions provided, -# you can craft \JSON additions of your own, -# either for Ruby built-in classes or for user-defined classes. -# -# Here's a user-defined class +Foo+: -# class Foo -# attr_accessor :bar, :baz -# def initialize(bar, baz) -# self.bar = bar -# self.baz = baz -# end -# end -# -# Here's the \JSON addition for it: -# # Extend class Foo with JSON addition. -# class Foo -# # Serialize Foo object with its class name and arguments -# def to_json(*args) -# { -# JSON.create_id => self.class.name, -# 'a' => [ bar, baz ] -# }.to_json(*args) -# end -# # Deserialize JSON string by constructing new Foo object with arguments. -# def self.json_create(object) -# new(*object['a']) -# end -# end -# -# Demonstration: -# require 'json' -# # This Foo object has no custom addition. -# foo0 = Foo.new(0, 1) -# json0 = JSON.generate(foo0) -# obj0 = JSON.parse(json0) -# # Lood the custom addition. -# require_relative 'foo_addition' -# # This foo has the custom addition. -# foo1 = Foo.new(0, 1) -# json1 = JSON.generate(foo1) -# obj1 = JSON.parse(json1, create_additions: true) -# # Make a nice display. -# display = <<~EOT -# Generated JSON: -# Without custom addition: #{json0} (#{json0.class}) -# With custom addition: #{json1} (#{json1.class}) -# Parsed JSON: -# Without custom addition: #{obj0.inspect} (#{obj0.class}) -# With custom addition: #{obj1.inspect} (#{obj1.class}) -# EOT -# puts display -# -# Output: -# -# Generated JSON: -# Without custom addition: "#" (String) -# With custom addition: {"json_class":"Foo","a":[0,1]} (String) -# Parsed JSON: -# Without custom addition: "#" (String) -# With custom addition: # (Foo) -# module JSON require 'json/version' require 'json/ext' diff --git a/lib/json/add/bigdecimal.rb b/lib/json/add/bigdecimal.rb deleted file mode 100644 index 5dbc12c07..000000000 --- a/lib/json/add/bigdecimal.rb +++ /dev/null @@ -1,58 +0,0 @@ -# frozen_string_literal: true -unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED - require 'json' -end -begin - require 'bigdecimal' -rescue LoadError -end - -class BigDecimal - - # See #as_json. - def self.json_create(object) - BigDecimal._load object['b'] - end - - # Methods BigDecimal#as_json and +BigDecimal.json_create+ may be used - # to serialize and deserialize a \BigDecimal object; - # see Marshal[https://docs.ruby-lang.org/en/master/Marshal.html]. - # - # \Method BigDecimal#as_json serializes +self+, - # returning a 2-element hash representing +self+: - # - # require 'json/add/bigdecimal' - # x = BigDecimal(2).as_json # => {"json_class"=>"BigDecimal", "b"=>"27:0.2e1"} - # y = BigDecimal(2.0, 4).as_json # => {"json_class"=>"BigDecimal", "b"=>"36:0.2e1"} - # z = BigDecimal(Complex(2, 0)).as_json # => {"json_class"=>"BigDecimal", "b"=>"27:0.2e1"} - # - # \Method +JSON.create+ deserializes such a hash, returning a \BigDecimal object: - # - # BigDecimal.json_create(x) # => 0.2e1 - # BigDecimal.json_create(y) # => 0.2e1 - # BigDecimal.json_create(z) # => 0.2e1 - # - def as_json(*) - { - JSON.create_id => self.class.name, - 'b' => _dump.force_encoding(Encoding::UTF_8), - } - end - - # Returns a JSON string representing +self+: - # - # require 'json/add/bigdecimal' - # puts BigDecimal(2).to_json - # puts BigDecimal(2.0, 4).to_json - # puts BigDecimal(Complex(2, 0)).to_json - # - # Output: - # - # {"json_class":"BigDecimal","b":"27:0.2e1"} - # {"json_class":"BigDecimal","b":"36:0.2e1"} - # {"json_class":"BigDecimal","b":"27:0.2e1"} - # - def to_json(*args) - as_json.to_json(*args) - end -end if defined?(::BigDecimal) diff --git a/lib/json/add/complex.rb b/lib/json/add/complex.rb deleted file mode 100644 index a69002eff..000000000 --- a/lib/json/add/complex.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true -unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED - require 'json' -end - -class Complex - - # See #as_json. - def self.json_create(object) - Complex(object['r'], object['i']) - end - - # Methods Complex#as_json and +Complex.json_create+ may be used - # to serialize and deserialize a \Complex object; - # see Marshal[https://docs.ruby-lang.org/en/master/Marshal.html]. - # - # \Method Complex#as_json serializes +self+, - # returning a 2-element hash representing +self+: - # - # require 'json/add/complex' - # x = Complex(2).as_json # => {"json_class"=>"Complex", "r"=>2, "i"=>0} - # y = Complex(2.0, 4).as_json # => {"json_class"=>"Complex", "r"=>2.0, "i"=>4} - # - # \Method +JSON.create+ deserializes such a hash, returning a \Complex object: - # - # Complex.json_create(x) # => (2+0i) - # Complex.json_create(y) # => (2.0+4i) - # - def as_json(*) - { - JSON.create_id => self.class.name, - 'r' => real, - 'i' => imag, - } - end - - # Returns a JSON string representing +self+: - # - # require 'json/add/complex' - # puts Complex(2).to_json - # puts Complex(2.0, 4).to_json - # - # Output: - # - # {"json_class":"Complex","r":2,"i":0} - # {"json_class":"Complex","r":2.0,"i":4} - # - def to_json(*args) - as_json.to_json(*args) - end -end diff --git a/lib/json/add/core.rb b/lib/json/add/core.rb deleted file mode 100644 index 61ff45421..000000000 --- a/lib/json/add/core.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true -# This file requires the implementations of ruby core's custom objects for -# serialisation/deserialisation. - -require 'json/add/date' -require 'json/add/date_time' -require 'json/add/exception' -require 'json/add/range' -require 'json/add/regexp' -require 'json/add/string' -require 'json/add/struct' -require 'json/add/symbol' -require 'json/add/time' diff --git a/lib/json/add/date.rb b/lib/json/add/date.rb deleted file mode 100644 index 66965d491..000000000 --- a/lib/json/add/date.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true -unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED - require 'json' -end -require 'date' - -class Date - - # See #as_json. - def self.json_create(object) - civil(*object.values_at('y', 'm', 'd', 'sg')) - end - - alias start sg unless method_defined?(:start) - - # Methods Date#as_json and +Date.json_create+ may be used - # to serialize and deserialize a \Date object; - # see Marshal[https://docs.ruby-lang.org/en/master/Marshal.html]. - # - # \Method Date#as_json serializes +self+, - # returning a 2-element hash representing +self+: - # - # require 'json/add/date' - # x = Date.today.as_json - # # => {"json_class"=>"Date", "y"=>2023, "m"=>11, "d"=>21, "sg"=>2299161.0} - # - # \Method +JSON.create+ deserializes such a hash, returning a \Date object: - # - # Date.json_create(x) - # # => # - # - def as_json(*) - { - JSON.create_id => self.class.name, - 'y' => year, - 'm' => month, - 'd' => day, - 'sg' => start, - } - end - - # Returns a JSON string representing +self+: - # - # require 'json/add/date' - # puts Date.today.to_json - # - # Output: - # - # {"json_class":"Date","y":2023,"m":11,"d":21,"sg":2299161.0} - # - def to_json(*args) - as_json.to_json(*args) - end -end diff --git a/lib/json/add/date_time.rb b/lib/json/add/date_time.rb deleted file mode 100644 index 569f6ec17..000000000 --- a/lib/json/add/date_time.rb +++ /dev/null @@ -1,67 +0,0 @@ -# frozen_string_literal: true -unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED - require 'json' -end -require 'date' - -class DateTime - - # See #as_json. - def self.json_create(object) - args = object.values_at('y', 'm', 'd', 'H', 'M', 'S') - of_a, of_b = object['of'].split('/') - if of_b and of_b != '0' - args << Rational(of_a.to_i, of_b.to_i) - else - args << of_a - end - args << object['sg'] - civil(*args) - end - - alias start sg unless method_defined?(:start) - - # Methods DateTime#as_json and +DateTime.json_create+ may be used - # to serialize and deserialize a \DateTime object; - # see Marshal[https://docs.ruby-lang.org/en/master/Marshal.html]. - # - # \Method DateTime#as_json serializes +self+, - # returning a 2-element hash representing +self+: - # - # require 'json/add/datetime' - # x = DateTime.now.as_json - # # => {"json_class"=>"DateTime", "y"=>2023, "m"=>11, "d"=>21, "sg"=>2299161.0} - # - # \Method +JSON.create+ deserializes such a hash, returning a \DateTime object: - # - # DateTime.json_create(x) # BUG? Raises Date::Error "invalid date" - # - def as_json(*) - { - JSON.create_id => self.class.name, - 'y' => year, - 'm' => month, - 'd' => day, - 'H' => hour, - 'M' => min, - 'S' => sec, - 'of' => offset.to_s, - 'sg' => start, - } - end - - # Returns a JSON string representing +self+: - # - # require 'json/add/datetime' - # puts DateTime.now.to_json - # - # Output: - # - # {"json_class":"DateTime","y":2023,"m":11,"d":21,"sg":2299161.0} - # - def to_json(*args) - as_json.to_json(*args) - end -end - - diff --git a/lib/json/add/exception.rb b/lib/json/add/exception.rb deleted file mode 100644 index 5338ff83d..000000000 --- a/lib/json/add/exception.rb +++ /dev/null @@ -1,49 +0,0 @@ -# frozen_string_literal: true -unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED - require 'json' -end - -class Exception - - # See #as_json. - def self.json_create(object) - result = new(object['m']) - result.set_backtrace object['b'] - result - end - - # Methods Exception#as_json and +Exception.json_create+ may be used - # to serialize and deserialize a \Exception object; - # see Marshal[https://docs.ruby-lang.org/en/master/Marshal.html]. - # - # \Method Exception#as_json serializes +self+, - # returning a 2-element hash representing +self+: - # - # require 'json/add/exception' - # x = Exception.new('Foo').as_json # => {"json_class"=>"Exception", "m"=>"Foo", "b"=>nil} - # - # \Method +JSON.create+ deserializes such a hash, returning a \Exception object: - # - # Exception.json_create(x) # => # - # - def as_json(*) - { - JSON.create_id => self.class.name, - 'm' => message, - 'b' => backtrace, - } - end - - # Returns a JSON string representing +self+: - # - # require 'json/add/exception' - # puts Exception.new('Foo').to_json - # - # Output: - # - # {"json_class":"Exception","m":"Foo","b":null} - # - def to_json(*args) - as_json.to_json(*args) - end -end diff --git a/lib/json/add/ostruct.rb b/lib/json/add/ostruct.rb deleted file mode 100644 index 534403b78..000000000 --- a/lib/json/add/ostruct.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true -unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED - require 'json' -end -begin - require 'ostruct' -rescue LoadError -end - -class OpenStruct - - # See #as_json. - def self.json_create(object) - new(object['t'] || object[:t]) - end - - # Methods OpenStruct#as_json and +OpenStruct.json_create+ may be used - # to serialize and deserialize a \OpenStruct object; - # see Marshal[https://docs.ruby-lang.org/en/master/Marshal.html]. - # - # \Method OpenStruct#as_json serializes +self+, - # returning a 2-element hash representing +self+: - # - # require 'json/add/ostruct' - # x = OpenStruct.new('name' => 'Rowdy', :age => nil).as_json - # # => {"json_class"=>"OpenStruct", "t"=>{:name=>'Rowdy', :age=>nil}} - # - # \Method +JSON.create+ deserializes such a hash, returning a \OpenStruct object: - # - # OpenStruct.json_create(x) - # # => # - # - def as_json(*) - klass = self.class.name - klass.to_s.empty? and raise JSON::JSONError, "Only named structs are supported!" - { - JSON.create_id => klass, - 't' => table, - } - end - - # Returns a JSON string representing +self+: - # - # require 'json/add/ostruct' - # puts OpenStruct.new('name' => 'Rowdy', :age => nil).to_json - # - # Output: - # - # {"json_class":"OpenStruct","t":{'name':'Rowdy',"age":null}} - # - def to_json(*args) - as_json.to_json(*args) - end -end if defined?(::OpenStruct) diff --git a/lib/json/add/range.rb b/lib/json/add/range.rb deleted file mode 100644 index eb4b29a8e..000000000 --- a/lib/json/add/range.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true -unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED - require 'json' -end - -class Range - - # See #as_json. - def self.json_create(object) - new(*object['a']) - end - - # Methods Range#as_json and +Range.json_create+ may be used - # to serialize and deserialize a \Range object; - # see Marshal[https://docs.ruby-lang.org/en/master/Marshal.html]. - # - # \Method Range#as_json serializes +self+, - # returning a 2-element hash representing +self+: - # - # require 'json/add/range' - # x = (1..4).as_json # => {"json_class"=>"Range", "a"=>[1, 4, false]} - # y = (1...4).as_json # => {"json_class"=>"Range", "a"=>[1, 4, true]} - # z = ('a'..'d').as_json # => {"json_class"=>"Range", "a"=>["a", "d", false]} - # - # \Method +JSON.create+ deserializes such a hash, returning a \Range object: - # - # Range.json_create(x) # => 1..4 - # Range.json_create(y) # => 1...4 - # Range.json_create(z) # => "a".."d" - # - def as_json(*) - { - JSON.create_id => self.class.name, - 'a' => [ first, last, exclude_end? ] - } - end - - # Returns a JSON string representing +self+: - # - # require 'json/add/range' - # puts (1..4).to_json - # puts (1...4).to_json - # puts ('a'..'d').to_json - # - # Output: - # - # {"json_class":"Range","a":[1,4,false]} - # {"json_class":"Range","a":[1,4,true]} - # {"json_class":"Range","a":["a","d",false]} - # - def to_json(*args) - as_json.to_json(*args) - end -end diff --git a/lib/json/add/rational.rb b/lib/json/add/rational.rb deleted file mode 100644 index 1eb231474..000000000 --- a/lib/json/add/rational.rb +++ /dev/null @@ -1,49 +0,0 @@ -# frozen_string_literal: true -unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED - require 'json' -end - -class Rational - - # See #as_json. - def self.json_create(object) - Rational(object['n'], object['d']) - end - - # Methods Rational#as_json and +Rational.json_create+ may be used - # to serialize and deserialize a \Rational object; - # see Marshal[https://docs.ruby-lang.org/en/master/Marshal.html]. - # - # \Method Rational#as_json serializes +self+, - # returning a 2-element hash representing +self+: - # - # require 'json/add/rational' - # x = Rational(2, 3).as_json - # # => {"json_class"=>"Rational", "n"=>2, "d"=>3} - # - # \Method +JSON.create+ deserializes such a hash, returning a \Rational object: - # - # Rational.json_create(x) - # # => (2/3) - # - def as_json(*) - { - JSON.create_id => self.class.name, - 'n' => numerator, - 'd' => denominator, - } - end - - # Returns a JSON string representing +self+: - # - # require 'json/add/rational' - # puts Rational(2, 3).to_json - # - # Output: - # - # {"json_class":"Rational","n":2,"d":3} - # - def to_json(*args) - as_json.to_json(*args) - end -end diff --git a/lib/json/add/regexp.rb b/lib/json/add/regexp.rb deleted file mode 100644 index f033dd1de..000000000 --- a/lib/json/add/regexp.rb +++ /dev/null @@ -1,48 +0,0 @@ -# frozen_string_literal: true -unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED - require 'json' -end - -class Regexp - - # See #as_json. - def self.json_create(object) - new(object['s'], object['o']) - end - - # Methods Regexp#as_json and +Regexp.json_create+ may be used - # to serialize and deserialize a \Regexp object; - # see Marshal[https://docs.ruby-lang.org/en/master/Marshal.html]. - # - # \Method Regexp#as_json serializes +self+, - # returning a 2-element hash representing +self+: - # - # require 'json/add/regexp' - # x = /foo/.as_json - # # => {"json_class"=>"Regexp", "o"=>0, "s"=>"foo"} - # - # \Method +JSON.create+ deserializes such a hash, returning a \Regexp object: - # - # Regexp.json_create(x) # => /foo/ - # - def as_json(*) - { - JSON.create_id => self.class.name, - 'o' => options, - 's' => source, - } - end - - # Returns a JSON string representing +self+: - # - # require 'json/add/regexp' - # puts /foo/.to_json - # - # Output: - # - # {"json_class":"Regexp","o":0,"s":"foo"} - # - def to_json(*args) - as_json.to_json(*args) - end -end diff --git a/lib/json/add/set.rb b/lib/json/add/set.rb deleted file mode 100644 index c521d8b90..000000000 --- a/lib/json/add/set.rb +++ /dev/null @@ -1,48 +0,0 @@ -unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED - require 'json' -end -defined?(::Set) or require 'set' - -class Set - - # See #as_json. - def self.json_create(object) - new object['a'] - end - - # Methods Set#as_json and +Set.json_create+ may be used - # to serialize and deserialize a \Set object; - # see Marshal[https://docs.ruby-lang.org/en/master/Marshal.html]. - # - # \Method Set#as_json serializes +self+, - # returning a 2-element hash representing +self+: - # - # require 'json/add/set' - # x = Set.new(%w/foo bar baz/).as_json - # # => {"json_class"=>"Set", "a"=>["foo", "bar", "baz"]} - # - # \Method +JSON.create+ deserializes such a hash, returning a \Set object: - # - # Set.json_create(x) # => # - # - def as_json(*) - { - JSON.create_id => self.class.name, - 'a' => to_a, - } - end - - # Returns a JSON string representing +self+: - # - # require 'json/add/set' - # puts Set.new(%w/foo bar baz/).to_json - # - # Output: - # - # {"json_class":"Set","a":["foo","bar","baz"]} - # - def to_json(*args) - as_json.to_json(*args) - end -end - diff --git a/lib/json/add/string.rb b/lib/json/add/string.rb deleted file mode 100644 index 9c3bde27f..000000000 --- a/lib/json/add/string.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true -unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED - require 'json' -end - -class String - # call-seq: json_create(o) - # - # Raw Strings are JSON Objects (the raw bytes are stored in an array for the - # key "raw"). The Ruby String can be created by this class method. - def self.json_create(object) - object["raw"].pack("C*") - end - - # call-seq: to_json_raw_object() - # - # This method creates a raw object hash, that can be nested into - # other data structures and will be generated as a raw string. This - # method should be used, if you want to convert raw strings to JSON - # instead of UTF-8 strings, e. g. binary data. - def to_json_raw_object - { - JSON.create_id => self.class.name, - "raw" => unpack("C*"), - } - end - - # call-seq: to_json_raw(*args) - # - # This method creates a JSON text from the result of a call to - # to_json_raw_object of this String. - def to_json_raw(...) - to_json_raw_object.to_json(...) - end -end diff --git a/lib/json/add/struct.rb b/lib/json/add/struct.rb deleted file mode 100644 index 98c38d326..000000000 --- a/lib/json/add/struct.rb +++ /dev/null @@ -1,52 +0,0 @@ -# frozen_string_literal: true -unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED - require 'json' -end - -class Struct - - # See #as_json. - def self.json_create(object) - new(*object['v']) - end - - # Methods Struct#as_json and +Struct.json_create+ may be used - # to serialize and deserialize a \Struct object; - # see Marshal[https://docs.ruby-lang.org/en/master/Marshal.html]. - # - # \Method Struct#as_json serializes +self+, - # returning a 2-element hash representing +self+: - # - # require 'json/add/struct' - # Customer = Struct.new('Customer', :name, :address, :zip) - # x = Struct::Customer.new.as_json - # # => {"json_class"=>"Struct::Customer", "v"=>[nil, nil, nil]} - # - # \Method +JSON.create+ deserializes such a hash, returning a \Struct object: - # - # Struct::Customer.json_create(x) - # # => # - # - def as_json(*) - klass = self.class.name - klass.to_s.empty? and raise JSON::JSONError, "Only named structs are supported!" - { - JSON.create_id => klass, - 'v' => values, - } - end - - # Returns a JSON string representing +self+: - # - # require 'json/add/struct' - # Customer = Struct.new('Customer', :name, :address, :zip) - # puts Struct::Customer.new.to_json - # - # Output: - # - # {"json_class":"Struct","t":{'name':'Rowdy',"age":null}} - # - def to_json(*args) - as_json.to_json(*args) - end -end diff --git a/lib/json/add/symbol.rb b/lib/json/add/symbol.rb deleted file mode 100644 index 20dd59485..000000000 --- a/lib/json/add/symbol.rb +++ /dev/null @@ -1,52 +0,0 @@ -# frozen_string_literal: true -unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED - require 'json' -end - -class Symbol - - # Methods Symbol#as_json and +Symbol.json_create+ may be used - # to serialize and deserialize a \Symbol object; - # see Marshal[https://docs.ruby-lang.org/en/master/Marshal.html]. - # - # \Method Symbol#as_json serializes +self+, - # returning a 2-element hash representing +self+: - # - # require 'json/add/symbol' - # x = :foo.as_json - # # => {"json_class"=>"Symbol", "s"=>"foo"} - # - # \Method +JSON.create+ deserializes such a hash, returning a \Symbol object: - # - # Symbol.json_create(x) # => :foo - # - def as_json(*) - { - JSON.create_id => self.class.name, - 's' => to_s, - } - end - - # Returns a JSON string representing +self+: - # - # require 'json/add/symbol' - # puts :foo.to_json - # - # Output: - # - # # {"json_class":"Symbol","s":"foo"} - # - def to_json(state = nil, *a) - state = ::JSON::State.from_state(state) - if state.strict? - super - else - as_json.to_json(state, *a) - end - end - - # See #as_json. - def self.json_create(o) - o['s'].to_sym - end -end diff --git a/lib/json/add/time.rb b/lib/json/add/time.rb deleted file mode 100644 index 05a1f242f..000000000 --- a/lib/json/add/time.rb +++ /dev/null @@ -1,52 +0,0 @@ -# frozen_string_literal: true -unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED - require 'json' -end - -class Time - - # See #as_json. - def self.json_create(object) - if usec = object.delete('u') # used to be tv_usec -> tv_nsec - object['n'] = usec * 1000 - end - at(object['s'], Rational(object['n'], 1000)) - end - - # Methods Time#as_json and +Time.json_create+ may be used - # to serialize and deserialize a \Time object; - # see Marshal[https://docs.ruby-lang.org/en/master/Marshal.html]. - # - # \Method Time#as_json serializes +self+, - # returning a 2-element hash representing +self+: - # - # require 'json/add/time' - # x = Time.now.as_json - # # => {"json_class"=>"Time", "s"=>1700931656, "n"=>472846644} - # - # \Method +JSON.create+ deserializes such a hash, returning a \Time object: - # - # Time.json_create(x) - # # => 2023-11-25 11:00:56.472846644 -0600 - # - def as_json(*) - { - JSON.create_id => self.class.name, - 's' => tv_sec, - 'n' => tv_nsec, - } - end - - # Returns a JSON string representing +self+: - # - # require 'json/add/time' - # puts Time.now.to_json - # - # Output: - # - # {"json_class":"Time","s":1700931678,"n":980650786} - # - def to_json(*args) - as_json.to_json(*args) - end -end diff --git a/lib/json/common.rb b/lib/json/common.rb index a030f0464..679222a05 100644 --- a/lib/json/common.rb +++ b/lib/json/common.rb @@ -17,10 +17,6 @@ def prepare(opts) opts[:on_load] = on_load end - if opts.fetch(:create_additions, false) != false - opts = create_additions_proc(opts) - end - opts end @@ -47,48 +43,6 @@ def array_class_proc(array_class, on_load) on_load.nil? ? obj : on_load.call(obj) end end - - # TODO: extract :create_additions support to another gem for version 3.0 - def create_additions_proc(opts) - if opts[:symbolize_names] - raise ArgumentError, "options :symbolize_names and :create_additions cannot be used in conjunction" - end - - opts = opts.dup - create_additions = opts.fetch(:create_additions, false) - on_load = opts[:on_load] - object_class = opts[:object_class] || Hash - - opts[:on_load] = ->(object) do - case object - when String - opts[:match_string]&.each do |pattern, klass| - if match = pattern.match(object) - object = klass.json_create(object) - break - end - end - when object_class - if opts[:create_additions] != false - if class_path = object[JSON.create_id] - klass = begin - Object.const_get(class_path) - rescue NameError => e - raise ArgumentError, "can't get const #{class_path}: #{e}" - end - - if klass.respond_to?(:json_creatable?) ? klass.json_creatable? : klass.respond_to?(:json_create) - object = klass.json_create(object) - end - end - end - end - - on_load.nil? ? object : on_load.call(object) - end - - opts - end end end @@ -182,19 +136,6 @@ def on_mixed_keys_hash(hash) end end - # Sets create identifier, which is used to decide if the _json_create_ - # hook of a class should be called; initial value is +json_class+: - # JSON.create_id # => 'json_class' - def self.create_id=(new_value) - Thread.current[:"JSON.create_id"] = new_value.dup.freeze - end - - # Returns the current create identifier. - # See also JSON.create_id=. - def self.create_id - Thread.current[:"JSON.create_id"] || 'json_class' - end - NaN = Float::NAN Infinity = Float::INFINITY @@ -491,7 +432,7 @@ def pretty_generate(obj, opts = nil) # # Returns the Ruby objects created by parsing the given +source+. # - # BEWARE: This method is meant to serialise data from trusted user input, + # BEWARE: This method is meant to deserialise data from trusted user input, # like from your own database server or clients under your control, it could # be dangerous to allow untrusted users to pass JSON sources into it. # @@ -620,7 +561,6 @@ def unsafe_load(source, proc = nil, options = nil) max_nesting: false, allow_nan: true, allow_blank: true, - create_additions: true, } if options.nil? && proc && proc.is_a?(Hash) options, proc = proc, nil @@ -656,16 +596,6 @@ def unsafe_load(source, proc = nil, options = nil) # # Returns the Ruby objects created by parsing the given +source+. # - # BEWARE: This method is meant to serialise data from trusted user input, - # like from your own database server or clients under your control, it could - # be dangerous to allow untrusted users to pass JSON sources into it. - # If you must use it, use JSON.unsafe_load instead to make it clear. - # - # Since JSON version 2.8.0, `load` emits a deprecation warning when a - # non native type is deserialized, without `create_additions` being explicitly - # enabled, and in JSON version 3.0, `load` will have `create_additions` disabled - # by default. - # # - Argument +source+ must be, or be convertible to, a \String: # - If +source+ responds to instance method +to_str+, # source.to_str becomes the source. diff --git a/lib/json/generic_object.rb b/lib/json/generic_object.rb index 5c8ace354..4d2144f93 100644 --- a/lib/json/generic_object.rb +++ b/lib/json/generic_object.rb @@ -10,12 +10,6 @@ class GenericObject < OpenStruct class << self alias [] new - def json_creatable? - @json_creatable - end - - attr_writer :json_creatable - def json_create(data) data = data.dup data.delete JSON.create_id @@ -46,7 +40,6 @@ def dump(obj, *args) ::JSON.dump(obj, *args) end end - self.json_creatable = false def to_hash table diff --git a/test/json/json_addition_test.rb b/test/json/json_addition_test.rb deleted file mode 100644 index fedd1f1d9..000000000 --- a/test/json/json_addition_test.rb +++ /dev/null @@ -1,186 +0,0 @@ -# frozen_string_literal: true -require_relative 'test_helper' -require 'json/add/core' -require 'json/add/complex' -require 'json/add/rational' -require 'json/add/bigdecimal' -require 'json/add/ostruct' -require 'json/add/set' -require 'date' - -class JSONAdditionTest < Test::Unit::TestCase - include JSON - - class A - def initialize(a) - @a = a - end - - attr_reader :a - - def ==(other) - a == other.a - end - - def self.json_create(object) - new(*object['args']) - end - - def to_json(*args) - { - 'json_class' => self.class.name, - 'args' => [ @a ], - }.to_json(*args) - end - end - - class A2 < A - def to_json(*args) - { - 'json_class' => self.class.name, - 'args' => [ @a ], - }.to_json(*args) - end - end - - class B - def to_json(*args) - { - 'json_class' => self.class.name, - }.to_json(*args) - end - end - - class C - def to_json(*args) - { - 'json_class' => 'JSONAdditionTest::Nix', - }.to_json(*args) - end - end - - def test_extended_json - a = A.new(666) - json = generate(a) - a_again = parse(json, :create_additions => true) - assert_kind_of a.class, a_again - assert_equal a, a_again - end - - def test_extended_json_default - a = A.new(666) - assert A.respond_to?(:json_create) - json = generate(a) - a_hash = parse(json) - assert_kind_of Hash, a_hash - end - - def test_extended_json_disabled - a = A.new(666) - json = generate(a) - a_again = parse(json, :create_additions => true) - assert_kind_of a.class, a_again - assert_equal a, a_again - a_hash = parse(json, :create_additions => false) - assert_kind_of Hash, a_hash - assert_equal( - {"args"=>[666], "json_class"=>"JSONAdditionTest::A"}.sort_by { |k,| k }, - a_hash.sort_by { |k,| k } - ) - end - - def test_extended_json_fail1 - b = B.new - json = generate(b) - assert_equal({ "json_class"=>"JSONAdditionTest::B" }, parse(json)) - end - - def test_extended_json_fail2 - c = C.new - json = generate(c) - assert_raise(ArgumentError, NameError) { parse(json, :create_additions => true) } - end - - def test_raw_strings - raw = ''.b - raw_array = [] - for i in 0..255 - raw << i - raw_array << i - end - json = raw.to_json_raw - json_raw_object = raw.to_json_raw_object - hash = { 'json_class' => 'String', 'raw'=> raw_array } - assert_equal hash, json_raw_object - assert_match(/\A\{.*\}\z/, json) - assert_match(/"json_class":"String"/, json) - assert_match(/"raw":\[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255\]/, json) - raw_again = parse(json, :create_additions => true) - assert_equal raw, raw_again - end - - MyJsonStruct = Struct.new 'MyJsonStruct', :foo, :bar - - def test_core - t = Time.now - assert_equal t, JSON(JSON(t), :create_additions => true) - d = Date.today - assert_equal d, JSON(JSON(d), :create_additions => true) - d = DateTime.civil(2007, 6, 14, 14, 57, 10, Rational(1, 12), 2299161) - assert_equal d, JSON(JSON(d), :create_additions => true) - assert_equal 1..10, JSON(JSON(1..10), :create_additions => true) - assert_equal 1...10, JSON(JSON(1...10), :create_additions => true) - assert_equal "a".."c", JSON(JSON("a".."c"), :create_additions => true) - assert_equal "a"..."c", JSON(JSON("a"..."c"), :create_additions => true) - s = MyJsonStruct.new 4711, 'foot' - assert_equal s, JSON(JSON(s), :create_additions => true) - struct = Struct.new :foo, :bar - s = struct.new 4711, 'foot' - assert_raise(JSONError) { JSON(s) } - begin - raise TypeError, "test me" - rescue TypeError => e - e_json = JSON.generate e - e_again = JSON e_json, :create_additions => true - assert_kind_of TypeError, e_again - assert_equal e.message, e_again.message - assert_equal e.backtrace, e_again.backtrace - end - assert_equal(/foo/, JSON(JSON(/foo/), :create_additions => true)) - assert_equal(/foo/i, JSON(JSON(/foo/i), :create_additions => true)) - end - - def test_utc_datetime - now = Time.now - d = DateTime.parse(now.to_s) # usual case - assert_equal d, parse(d.to_json, :create_additions => true) - d = DateTime.parse(now.utc.to_s) # of = 0 - assert_equal d, parse(d.to_json, :create_additions => true) - d = DateTime.civil(2008, 6, 17, 11, 48, 32, Rational(1,24)) - assert_equal d, parse(d.to_json, :create_additions => true) - d = DateTime.civil(2008, 6, 17, 11, 48, 32, Rational(12,24)) - assert_equal d, parse(d.to_json, :create_additions => true) - end - - def test_rational_complex - assert_equal Rational(2, 9), parse(JSON(Rational(2, 9)), :create_additions => true) - assert_equal Complex(2, 9), parse(JSON(Complex(2, 9)), :create_additions => true) - end - - def test_bigdecimal - assert_equal BigDecimal('3.141', 23), JSON(JSON(BigDecimal('3.141', 23)), :create_additions => true) - assert_equal BigDecimal('3.141', 666), JSON(JSON(BigDecimal('3.141', 666)), :create_additions => true) - end if defined?(::BigDecimal) - - def test_ostruct - o = OpenStruct.new - # XXX this won't work; o.foo = { :bar => true } - o.foo = { 'bar' => true } - assert_equal o, parse(JSON(o), :create_additions => true) - end if defined?(::OpenStruct) - - def test_set - s = Set.new([:a, :b, :c, :a]) - assert_equal s, JSON.parse(JSON(s), :create_additions => true) - end -end diff --git a/test/json/json_common_interface_test.rb b/test/json/json_common_interface_test.rb index 72b8f390a..34ed27c69 100644 --- a/test/json/json_common_interface_test.rb +++ b/test/json/json_common_interface_test.rb @@ -54,14 +54,6 @@ def test_state assert_match(/::(TruffleRuby)?Generator::State\z/, JSON.state.name) end - def test_create_id - assert_equal 'json_class', JSON.create_id - JSON.create_id = 'foo_bar' - assert_equal 'foo_bar', JSON.create_id - ensure - JSON.create_id = 'json_class' - end - def test_parse assert_equal [ 1, 2, 3, ], JSON.parse('[ 1, 2, 3 ]') end diff --git a/test/json/json_generic_object_test.rb b/test/json/json_generic_object_test.rb index 57e3bf3c5..01ae71e56 100644 --- a/test/json/json_generic_object_test.rb +++ b/test/json/json_generic_object_test.rb @@ -26,32 +26,13 @@ def test_attributes assert_nil @go[:c] end - def test_generate_json - switch_json_creatable do - assert_equal @go, JSON(JSON(@go), :create_additions => true) - end - end - def test_parse_json - assert_kind_of Hash, - JSON( - '{ "json_class": "JSON::GenericObject", "a": 1, "b": 2 }', - :create_additions => true - ) - switch_json_creatable do - assert_equal @go, l = - JSON( - '{ "json_class": "JSON::GenericObject", "a": 1, "b": 2 }', - :create_additions => true - ) - assert_equal 1, l.a - assert_equal @go, - l = JSON('{ "a": 1, "b": 2 }', :object_class => JSON::GenericObject) - assert_equal 1, l.a - assert_equal JSON::GenericObject[:a => JSON::GenericObject[:b => 2]], - l = JSON('{ "a": { "b": 2 } }', :object_class => JSON::GenericObject) - assert_equal 2, l.a.b - end + assert_equal @go, + l = JSON('{ "a": 1, "b": 2 }', :object_class => JSON::GenericObject) + assert_equal 1, l.a + assert_equal JSON::GenericObject[:a => JSON::GenericObject[:b => 2]], + l = JSON('{ "a": { "b": 2 } }', :object_class => JSON::GenericObject) + assert_equal 2, l.a.b end def test_from_hash @@ -79,13 +60,4 @@ def test_json_generic_object_load json = JSON::GenericObject.dump(JSON::GenericObject[:hello => 'world']) assert_equal JSON(json), JSON('{"json_class":"JSON::GenericObject","hello":"world"}') end - - private - - def switch_json_creatable - JSON::GenericObject.json_creatable = true - yield - ensure - JSON::GenericObject.json_creatable = false - end end diff --git a/test/json/json_parser_test.rb b/test/json/json_parser_test.rb index 1f257e71d..03a29d0e8 100644 --- a/test/json/json_parser_test.rb +++ b/test/json/json_parser_test.rb @@ -477,9 +477,6 @@ def test_symbolize_names parse('{"foo":"bar", "baz":"quux"}')) assert_equal({ :foo => "bar", :baz => "quux" }, parse('{"foo":"bar", "baz":"quux"}', :symbolize_names => true)) - assert_raise(ArgumentError) do - parse('{}', :symbolize_names => true, :create_additions => true) - end end def test_freeze @@ -687,20 +684,6 @@ def shifted? end end - class SubArray2 < Array - def to_json(*a) - { - JSON.create_id => self.class.name, - 'ary' => to_a, - }.to_json(*a) - end - - def self.json_create(o) - o.delete JSON.create_id - o['ary'] - end - end - class SubArrayWrapper def initialize @data = [] @@ -813,18 +796,6 @@ def test_parse_generic_object end end - def test_generate_core_subclasses_with_new_to_json - obj = SubHash2["foo" => SubHash2["bar" => true]] - obj_json = JSON(obj) - obj_again = parse(obj_json, :create_additions => true) - assert_kind_of SubHash2, obj_again - assert_kind_of SubHash2, obj_again['foo'] - assert obj_again['foo']['bar'] - assert_equal obj, obj_again - assert_equal ["foo"], - JSON(JSON(SubArray2["foo"]), :create_additions => true) - end - def test_generate_core_subclasses_with_default_to_json assert_equal '{"foo":"bar"}', JSON(SubHash["foo" => "bar"]) assert_equal '["foo"]', JSON(SubArray["foo"]) diff --git a/test/json/json_string_matching_test.rb b/test/json/json_string_matching_test.rb deleted file mode 100644 index 21cd64902..000000000 --- a/test/json/json_string_matching_test.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true -require_relative 'test_helper' -require 'time' - -class JSONStringMatchingTest < Test::Unit::TestCase - include JSON - - class TestTime < ::Time - def self.json_create(string) - Time.parse(string) - end - - def to_json(*) - %{"#{strftime('%FT%T%z')}"} - end - - def ==(other) - to_i == other.to_i - end - end - - def test_match_date - t = TestTime.new - t_json = [ t ].to_json - time_regexp = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{4}\z/ - assert_equal [ t ], - parse( - t_json, - :create_additions => true, - :match_string => { time_regexp => TestTime } - ) - assert_equal [ t.strftime('%FT%T%z') ], - parse( - t_json, - :match_string => { time_regexp => TestTime } - ) - end -end From 6a3c3ffbcd970d0dadc08f6e943f96410c573a34 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Mon, 27 Jul 2026 21:04:13 +0200 Subject: [PATCH 09/18] Remove JSON.fast_generate --- CHANGES.md | 1 + lib/json/common.rb | 22 -------------- test/json/json_generator_test.rb | 52 +++++++++++--------------------- 3 files changed, 19 insertions(+), 56 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 382428b19..c0d4462cc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ * Removed `JSON.dump_default_options`. * Removed `JSON::State#[]` and `JSON::State#[]=`. * Removed `JSON.unparse`. +* Removed `JSON.fast_generate`. * Removed `JSON.fast_unparse`. * Removed `JSON.pretty_unparse`. * Removed `JSON.restore`. diff --git a/lib/json/common.rb b/lib/json/common.rb index 679222a05..27a489c11 100644 --- a/lib/json/common.rb +++ b/lib/json/common.rb @@ -345,28 +345,6 @@ def generate(obj, opts = nil) end end - # :call-seq: - # JSON.fast_generate(obj, opts) -> new_string - # - # Arguments +obj+ and +opts+ here are the same as - # arguments +obj+ and +opts+ in JSON.generate. - # - # By default, generates \JSON data without checking - # for circular references in +obj+ (option +max_nesting+ set to +false+, disabled). - # - # Raises an exception if +obj+ contains circular references: - # a = []; b = []; a.push(b); b.push(a) - # # Raises SystemStackError (stack level too deep): - # JSON.fast_generate(a) - def fast_generate(obj, opts = nil) - if RUBY_VERSION >= "3.0" - warn "JSON.fast_generate is deprecated and will be removed in json 3.0.0, just use JSON.generate", uplevel: 1, category: :deprecated - else - warn "JSON.fast_generate is deprecated and will be removed in json 3.0.0, just use JSON.generate", uplevel: 1 - end - generate(obj, opts) - end - PRETTY_GENERATE_OPTIONS = { indent: ' ', space: ' ', diff --git a/test/json/json_generator_test.rb b/test/json/json_generator_test.rb index 937f71651..f124e22b6 100755 --- a/test/json/json_generator_test.rb +++ b/test/json/json_generator_test.rb @@ -256,20 +256,6 @@ def test_generate_custom JSON end - def test_fast_generate - assert_deprecated_warning(/fast_generate/) do - json = fast_generate(@hash) - assert_equal(parse(@json2), parse(json)) - parsed_json = parse(json) - assert_equal(@hash, parsed_json) - json = fast_generate({1=>2}) - assert_equal('{"1":2}', json) - parsed_json = parse(json) - assert_equal({"1"=>2}, parsed_json) - assert_equal '666', fast_generate(666) - end - end - def test_own_state state = State.new json = generate(@hash, state) @@ -359,26 +345,24 @@ def test_state_defaults end def test_allow_nan - assert_deprecated_warning(/fast_generate/) do - error = assert_raise(GeneratorError) { generate([JSON::NaN]) } - assert_same JSON::NaN, error.invalid_object - assert_equal '[NaN]', generate([JSON::NaN], :allow_nan => true) - assert_raise(GeneratorError) { fast_generate([JSON::NaN]) } - assert_raise(GeneratorError) { pretty_generate([JSON::NaN]) } - assert_equal "[\n NaN\n]", pretty_generate([JSON::NaN], :allow_nan => true) - error = assert_raise(GeneratorError) { generate([JSON::Infinity]) } - assert_same JSON::Infinity, error.invalid_object - assert_equal '[Infinity]', generate([JSON::Infinity], :allow_nan => true) - assert_raise(GeneratorError) { fast_generate([JSON::Infinity]) } - assert_raise(GeneratorError) { pretty_generate([JSON::Infinity]) } - assert_equal "[\n Infinity\n]", pretty_generate([JSON::Infinity], :allow_nan => true) - error = assert_raise(GeneratorError) { generate([JSON::MinusInfinity]) } - assert_same JSON::MinusInfinity, error.invalid_object - assert_equal '[-Infinity]', generate([JSON::MinusInfinity], :allow_nan => true) - assert_raise(GeneratorError) { fast_generate([JSON::MinusInfinity]) } - assert_raise(GeneratorError) { pretty_generate([JSON::MinusInfinity]) } - assert_equal "[\n -Infinity\n]", pretty_generate([JSON::MinusInfinity], :allow_nan => true) - end + error = assert_raise(GeneratorError) { generate([JSON::NaN]) } + assert_same JSON::NaN, error.invalid_object + assert_equal '[NaN]', generate([JSON::NaN], :allow_nan => true) + assert_raise(GeneratorError) { generate([JSON::NaN]) } + assert_raise(GeneratorError) { pretty_generate([JSON::NaN]) } + assert_equal "[\n NaN\n]", pretty_generate([JSON::NaN], :allow_nan => true) + error = assert_raise(GeneratorError) { generate([JSON::Infinity]) } + assert_same JSON::Infinity, error.invalid_object + assert_equal '[Infinity]', generate([JSON::Infinity], :allow_nan => true) + assert_raise(GeneratorError) { generate([JSON::Infinity]) } + assert_raise(GeneratorError) { pretty_generate([JSON::Infinity]) } + assert_equal "[\n Infinity\n]", pretty_generate([JSON::Infinity], :allow_nan => true) + error = assert_raise(GeneratorError) { generate([JSON::MinusInfinity]) } + assert_same JSON::MinusInfinity, error.invalid_object + assert_equal '[-Infinity]', generate([JSON::MinusInfinity], :allow_nan => true) + assert_raise(GeneratorError) { generate([JSON::MinusInfinity]) } + assert_raise(GeneratorError) { pretty_generate([JSON::MinusInfinity]) } + assert_equal "[\n -Infinity\n]", pretty_generate([JSON::MinusInfinity], :allow_nan => true) end # An object that changes state.depth when it receives to_json(state) From c6bae1f92dbf6d4da39233b8d3532024ef24ac72 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Mon, 27 Jul 2026 20:57:45 +0200 Subject: [PATCH 10/18] Remove JSON::GenericObject --- CHANGES.md | 1 + Gemfile | 1 - lib/json/common.rb | 2 - lib/json/generic_object.rb | 60 ------------------------- test/json/json_generic_object_test.rb | 63 --------------------------- test/json/json_parser_test.rb | 53 ++++++++-------------- 6 files changed, 20 insertions(+), 160 deletions(-) delete mode 100644 lib/json/generic_object.rb delete mode 100644 test/json/json_generic_object_test.rb diff --git a/CHANGES.md b/CHANGES.md index c0d4462cc..5479d7965 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,6 +16,7 @@ * Removed `JSON.restore`. * Removed `JSON::PRETTY_STATE_PROTOTYPE`. * Removed the insecure `create_additions` option. +* Removed `JSON::GenericObject`. ### 2026-07-31 (2.21.2) diff --git a/Gemfile b/Gemfile index 7956c2dc2..407a3f9b0 100644 --- a/Gemfile +++ b/Gemfile @@ -5,7 +5,6 @@ gemspec group :development do gem "ruby_memcheck" if RUBY_PLATFORM =~ /linux/i gem "bigdecimal" - gem "ostruct" gem "rake" gem "rake-compiler" gem "test-unit" diff --git a/lib/json/common.rb b/lib/json/common.rb index 27a489c11..78dc867dd 100644 --- a/lib/json/common.rb +++ b/lib/json/common.rb @@ -3,8 +3,6 @@ require 'json/version' module JSON - autoload :GenericObject, 'json/generic_object' - module ParserOptions # :nodoc: class << self def prepare(opts) diff --git a/lib/json/generic_object.rb b/lib/json/generic_object.rb deleted file mode 100644 index 4d2144f93..000000000 --- a/lib/json/generic_object.rb +++ /dev/null @@ -1,60 +0,0 @@ -# frozen_string_literal: true -begin - require 'ostruct' -rescue LoadError - warn "JSON::GenericObject requires 'ostruct'. Please install it with `gem install ostruct`." -end - -module JSON - class GenericObject < OpenStruct - class << self - alias [] new - - def json_create(data) - data = data.dup - data.delete JSON.create_id - self[data] - end - - def from_hash(object) - case - when object.respond_to?(:to_hash) - result = new - object.to_hash.each do |key, value| - result[key] = from_hash(value) - end - result - when object.respond_to?(:to_ary) - object.to_ary.map { |a| from_hash(a) } - else - object - end - end - - def load(source, proc = nil, opts = {}) - result = ::JSON.load(source, proc, opts.merge(:object_class => self)) - result.nil? ? new : result - end - - def dump(obj, *args) - ::JSON.dump(obj, *args) - end - end - - def to_hash - table - end - - def |(other) - self.class[other.to_hash.merge(to_hash)] - end - - def as_json(*) - { JSON.create_id => self.class.name }.merge to_hash - end - - def to_json(*a) - as_json.to_json(*a) - end - end if defined?(::OpenStruct) -end diff --git a/test/json/json_generic_object_test.rb b/test/json/json_generic_object_test.rb deleted file mode 100644 index 01ae71e56..000000000 --- a/test/json/json_generic_object_test.rb +++ /dev/null @@ -1,63 +0,0 @@ -# frozen_string_literal: true -require_relative 'test_helper' - -# ostruct is required to test JSON::GenericObject -begin - require "ostruct" -rescue LoadError - return -end - -class JSONGenericObjectTest < Test::Unit::TestCase - def setup - if defined?(JSON::GenericObject) - @go = JSON::GenericObject[ :a => 1, :b => 2 ] - else - omit("JSON::GenericObject is not available") - end - end - - def test_attributes - assert_equal 1, @go.a - assert_equal 1, @go[:a] - assert_equal 2, @go.b - assert_equal 2, @go[:b] - assert_nil @go.c - assert_nil @go[:c] - end - - def test_parse_json - assert_equal @go, - l = JSON('{ "a": 1, "b": 2 }', :object_class => JSON::GenericObject) - assert_equal 1, l.a - assert_equal JSON::GenericObject[:a => JSON::GenericObject[:b => 2]], - l = JSON('{ "a": { "b": 2 } }', :object_class => JSON::GenericObject) - assert_equal 2, l.a.b - end - - def test_from_hash - result = JSON::GenericObject.from_hash( - :foo => { :bar => { :baz => true }, :quux => [ { :foobar => true } ] }) - assert_kind_of JSON::GenericObject, result.foo - assert_kind_of JSON::GenericObject, result.foo.bar - assert_equal true, result.foo.bar.baz - assert_kind_of JSON::GenericObject, result.foo.quux.first - assert_equal true, result.foo.quux.first.foobar - assert_equal true, JSON::GenericObject.from_hash(true) - end - - def test_json_generic_object_load - empty = JSON::GenericObject.load(nil) - assert_kind_of JSON::GenericObject, empty - simple_json = '{"json_class":"JSON::GenericObject","hello":"world"}' - simple = JSON::GenericObject.load(simple_json) - assert_kind_of JSON::GenericObject, simple - assert_equal "world", simple.hello - converting = JSON::GenericObject.load('{ "hello": "world" }') - assert_kind_of JSON::GenericObject, converting - assert_equal "world", converting.hello - - json = JSON::GenericObject.dump(JSON::GenericObject[:hello => 'world']) - assert_equal JSON(json), JSON('{"json_class":"JSON::GenericObject","hello":"world"}') - end -end diff --git a/test/json/json_parser_test.rb b/test/json/json_parser_test.rb index 03a29d0e8..1819d0abf 100644 --- a/test/json/json_parser_test.rb +++ b/test/json/json_parser_test.rb @@ -2,10 +2,6 @@ require_relative 'test_helper' require 'stringio' require 'tempfile' -begin - require 'ostruct' -rescue LoadError -end begin require 'bigdecimal' rescue LoadError @@ -758,44 +754,33 @@ def test_parse_object_custom_hash_derived_class assert res.item_set? end - if defined?(::OpenStruct) - class SubOpenStruct < OpenStruct - def [](k) - __send__(k) - end - - def []=(k, v) - @item_set = true - __send__("#{k}=", v) - end + class OpenStructLike + def initialize + @attrs = {} + end - def item_set? - @item_set - end + def [](k) + @attrs[k.to_sym] end - def test_parse_object_custom_non_hash_derived_class - res = parse('{"foo":"bar"}', :object_class => SubOpenStruct) - assert_equal "bar", res.foo - assert_equal "bar", res[:foo] - assert_equal(SubOpenStruct, res.class) - assert res.item_set? + def []=(k, v) + @attrs[k.to_sym] = v end - def test_parse_generic_object - res = parse( - '{"foo":"bar", "baz":{}}', - :object_class => JSON::GenericObject - ) - assert_equal(JSON::GenericObject, res.class) - assert_equal "bar", res.foo - assert_equal "bar", res["foo"] - assert_equal "bar", res[:foo] - assert_equal "bar", res.to_hash[:foo] - assert_equal(JSON::GenericObject, res.baz.class) + def method_missing(name, ...) + @attrs.fetch(name) do + super + end end end + def test_parse_object_custom_non_hash_derived_class + res = parse('{"foo":"bar"}', :object_class => OpenStructLike) + assert_equal "bar", res.foo + assert_equal "bar", res[:foo] + assert_equal(OpenStructLike, res.class) + end + def test_generate_core_subclasses_with_default_to_json assert_equal '{"foo":"bar"}', JSON(SubHash["foo" => "bar"]) assert_equal '["foo"]', JSON(SubArray["foo"]) From 72a14e0ecd1b54bbcae5418478da327887e39e5e Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Mon, 10 Aug 2026 16:10:02 +0200 Subject: [PATCH 11/18] Remove `escape_slash` --- CHANGES.md | 1 + ext/json/ext/generator/generator.c | 7 +------ java/src/json/ext/GeneratorState.java | 9 +++------ lib/json/truffle_ruby/generator.rb | 2 -- test/json/json_generator_test.rb | 2 +- 5 files changed, 6 insertions(+), 15 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 5479d7965..1f0f9111e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ * The `allow_comments` parsing option now default to `false`. * The `allow_duplicate_key` option now defaults to `false`, for both parsing and generating JSON. +* Removed the `escape_slash` alias of `script_safe`. * Removed `Kernel#j` and `Kernel#jj`. * Removed `JSON.load_default_options`. * Removed `JSON.unsafe_load_default_options`. diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c index a45fd3928..753a5656e 100644 --- a/ext/json/ext/generator/generator.c +++ b/ext/json/ext/generator/generator.c @@ -34,7 +34,7 @@ static VALUE mJSON, cState, cFragment, eGeneratorError, eNestingError, Encoding_ static ID i_to_s, i_to_json, i_new, i_encode; 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, sym_sort_keys; + sym_ascii_only, sym_depth, sym_buffer_initial_length, sym_script_safe, sym_strict, sym_as_json, sym_sort_keys; #define GET_STATE_TO(self, state) \ @@ -1868,7 +1868,6 @@ static int configure_state_i(VALUE key, VALUE val, VALUE _arg) else if (key == sym_depth) { state->depth = depth_config(val); } else if (key == sym_buffer_initial_length) { buffer_initial_length_set(state, val); } 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->allow_duplicate_key = RTEST(val); } else if (key == sym_as_json) { @@ -1991,9 +1990,6 @@ void Init_generator(void) rb_define_method(cState, "script_safe", cState_script_safe, 0); rb_define_method(cState, "script_safe?", cState_script_safe, 0); rb_define_method(cState, "script_safe=", cState_script_safe_set, 1); - rb_define_alias(cState, "escape_slash", "script_safe"); - rb_define_alias(cState, "escape_slash?", "script_safe?"); - rb_define_alias(cState, "escape_slash=", "script_safe="); rb_define_method(cState, "strict", cState_strict, 0); rb_define_method(cState, "strict?", cState_strict, 0); rb_define_method(cState, "strict=", cState_strict_set, 1); @@ -2035,7 +2031,6 @@ void Init_generator(void) sym_depth = ID2SYM(rb_intern("depth")); sym_buffer_initial_length = ID2SYM(rb_intern("buffer_initial_length")); sym_script_safe = ID2SYM(rb_intern("script_safe")); - 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")); diff --git a/java/src/json/ext/GeneratorState.java b/java/src/json/ext/GeneratorState.java index 1086bf6d3..143cf2d7c 100644 --- a/java/src/json/ext/GeneratorState.java +++ b/java/src/json/ext/GeneratorState.java @@ -397,19 +397,19 @@ public boolean scriptSafe() { return scriptSafe; } - @JRubyMethod(name="script_safe", alias="escape_slash") + @JRubyMethod(name="script_safe") public RubyBoolean script_safe_get(ThreadContext context) { return RubyBoolean.newBoolean(context, scriptSafe); } - @JRubyMethod(name="script_safe=", alias="escape_slash=") + @JRubyMethod(name="script_safe=") public IRubyObject script_safe_set(IRubyObject script_safe) { checkFrozen(); scriptSafe = script_safe.isTrue(); return script_safe.getRuntime().newBoolean(scriptSafe); } - @JRubyMethod(name="script_safe?", alias="escape_slash?") + @JRubyMethod(name="script_safe?") public RubyBoolean script_safe_p(ThreadContext context) { return RubyBoolean.newBoolean(context, scriptSafe); } @@ -566,9 +566,6 @@ public IRubyObject _configure(ThreadContext context, IRubyObject vOpts) { allowNaN = opts.getBool("allow_nan", DEFAULT_ALLOW_NAN); asciiOnly = opts.getBool("ascii_only", DEFAULT_ASCII_ONLY); scriptSafe = opts.getBool("script_safe", DEFAULT_SCRIPT_SAFE); - if (!scriptSafe) { - scriptSafe = opts.getBool("escape_slash", DEFAULT_SCRIPT_SAFE); - } strict = opts.getBool("strict", DEFAULT_STRICT); bufferInitialLength = opts.getInt("buffer_initial_length", DEFAULT_BUFFER_INITIAL_LENGTH); diff --git a/lib/json/truffle_ruby/generator.rb b/lib/json/truffle_ruby/generator.rb index 495167be2..4f033ac58 100644 --- a/lib/json/truffle_ruby/generator.rb +++ b/lib/json/truffle_ruby/generator.rb @@ -323,8 +323,6 @@ def configure(opts) @script_safe = if opts.key?(:script_safe) !!opts[:script_safe] - elsif opts.key?(:escape_slash) - !!opts[:escape_slash] else false end diff --git a/test/json/json_generator_test.rb b/test/json/json_generator_test.rb index f124e22b6..7ae415cf5 100755 --- a/test/json/json_generator_test.rb +++ b/test/json/json_generator_test.rb @@ -651,7 +651,7 @@ def test_backslash # data = [ "/\u2028\u2029" ] json = '["\/\u2028\u2029"]' - assert_equal json, generate(data, :escape_slash => true) + assert_equal json, generate(data, :script_safe => true) # data = ['"'] json = '["\""]' From 3d9aef1d46d500be69ce919af59703e9c777f201 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Mon, 10 Aug 2026 16:14:34 +0200 Subject: [PATCH 12/18] Remove the `limit` positional argument of `JSON.dump`. It's entirely redundant with `max_nesting`, makes little sense except to match Marshal signature exactly, but I don't see the point. --- CHANGES.md | 1 + lib/json/common.rb | 31 ++++++--------------- test/json/json_common_interface_test.rb | 36 +++++++++++-------------- 3 files changed, 25 insertions(+), 43 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 1f0f9111e..98893bc68 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ * The `allow_comments` parsing option now default to `false`. * The `allow_duplicate_key` option now defaults to `false`, for both parsing and generating JSON. +* Removed the `limit` positional argument of `JSON.dump`. * Removed the `escape_slash` alias of `script_safe`. * Removed `Kernel#j` and `Kernel#jj`. * Removed `JSON.load_default_options`. diff --git a/lib/json/common.rb b/lib/json/common.rb index 78dc867dd..f796a0c81 100644 --- a/lib/json/common.rb +++ b/lib/json/common.rb @@ -728,7 +728,7 @@ def load(source, proc = nil, options = nil) end # :call-seq: - # JSON.dump(obj, io = nil, limit = nil) + # JSON.dump(obj, io = nil, options = nil) # # Dumps +obj+ as a \JSON string, i.e. calls generate on the object and returns the result. # @@ -737,7 +737,6 @@ def load(source, proc = nil, options = nil) # - Argument +io+, if given, should respond to method +write+; # the \JSON \String is written to +io+, and +io+ is returned. # If +io+ is not given, the \JSON \String is returned. - # - Argument +limit+, if given, is passed to JSON.generate as option +max_nesting+. # # --- # @@ -754,38 +753,24 @@ def load(source, proc = nil, options = nil) # puts File.read(path) # Output: # {"foo":[0,1],"bar":{"baz":2,"bat":3},"bam":"bad"} - def dump(obj, anIO = nil, limit = nil, kwargs = nil) + def dump(obj, anIO = nil, kwargs = nil) if kwargs.nil? - if limit.nil? - if anIO.is_a?(Hash) - kwargs = anIO - anIO = nil - end - elsif limit.is_a?(Hash) - kwargs = limit - limit = nil + if anIO.is_a?(Hash) + kwargs = anIO + anIO = nil end end - unless anIO.nil? - if anIO.respond_to?(:to_io) - anIO = anIO.to_io - elsif limit.nil? && !anIO.respond_to?(:write) - anIO, limit = nil, anIO - end + if anIO&.respond_to?(:to_io) + anIO = anIO.to_io end opts = { allow_nan: true, - max_nesting: limit, } opts.merge!(kwargs) if kwargs - begin - State.generate(obj, opts, anIO) - rescue JSON::NestingError - raise ArgumentError, "exceed depth limit" - end + State.generate(obj, opts, anIO) end # JSON::Coder holds a parser and generator configuration. diff --git a/test/json/json_common_interface_test.rb b/test/json/json_common_interface_test.rb index 34ed27c69..9b0802de5 100644 --- a/test/json/json_common_interface_test.rb +++ b/test/json/json_common_interface_test.rb @@ -229,27 +229,23 @@ def test_unsafe_load_null end def test_dump - too_deep = '[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]' + too_deep = '[' * 101 + ']' * 101 obj = eval(too_deep) - assert_equal too_deep, dump(obj) - assert_kind_of String, Marshal.dump(obj) - assert_raise(ArgumentError) { dump(obj, 100) } - assert_raise(ArgumentError) { Marshal.dump(obj, 100) } - assert_equal too_deep, dump(obj, 101) - assert_kind_of String, Marshal.dump(obj, 101) - - assert_equal too_deep, JSON.dump(obj, StringIO.new, 101, strict: false).string - assert_equal too_deep, dump(obj, StringIO.new, 101, strict: false).string - assert_raise(JSON::GeneratorError) { JSON.dump(Object.new, StringIO.new, 101, strict: true).string } - assert_raise(JSON::GeneratorError) { dump(Object.new, StringIO.new, 101, strict: true).string } - - assert_equal too_deep, dump(obj, nil, nil, strict: false) - assert_equal too_deep, dump(obj, nil, 101, strict: false) - assert_equal too_deep, dump(obj, StringIO.new, nil, strict: false).string - assert_equal too_deep, dump(obj, nil, strict: false) - assert_equal too_deep, dump(obj, 101, strict: false) - assert_equal too_deep, dump(obj, StringIO.new, strict: false).string - assert_equal too_deep, dump(obj, strict: false) + assert_raise(JSON::NestingError) { dump(obj) } + assert_equal too_deep, dump(obj, max_nesting: 101) + + assert_equal too_deep, JSON.dump(obj, StringIO.new, max_nesting: 101, strict: false).string + assert_equal too_deep, dump(obj, StringIO.new, max_nesting: 101, strict: false).string + assert_raise(JSON::GeneratorError) { JSON.dump(Object.new, StringIO.new, max_nesting: 101, strict: true).string } + assert_raise(JSON::GeneratorError) { dump(Object.new, StringIO.new, max_nesting: 101, strict: true).string } + + assert_raise(JSON::NestingError) { dump(obj, nil, strict: false) } + assert_equal too_deep, dump(obj, nil, max_nesting: 101, strict: false) + assert_raise(JSON::NestingError) { dump(obj, StringIO.new, strict: false) } + assert_raise(JSON::NestingError) { dump(obj, strict: false) } + assert_equal too_deep, dump(obj, max_nesting: 101, strict: false) + assert_raise(JSON::NestingError) { dump(obj, StringIO.new, strict: false) } + assert_raise(JSON::NestingError) { dump(obj, strict: false) } end def test_dump_in_io From dfdefc1c91dcdf4fa32b108cdc9f171308da296b Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Mon, 10 Aug 2026 12:00:31 +0200 Subject: [PATCH 13/18] Make parsing options keyword arguments Or at least behave like them. --- ext/json/ext/parser/parser.c | 2 +- lib/json/common.rb | 156 +++++++++--------------- test/json/json_common_interface_test.rb | 7 ++ 3 files changed, 68 insertions(+), 97 deletions(-) diff --git a/ext/json/ext/parser/parser.c b/ext/json/ext/parser/parser.c index 54ecb2c89..8b6c04efe 100644 --- a/ext/json/ext/parser/parser.c +++ b/ext/json/ext/parser/parser.c @@ -2015,7 +2015,7 @@ static int parser_config_init_i(VALUE key, VALUE val, VALUE data) } } } - else if (args->strict) { + else { if (!args->unknown_keywords) { args->unknown_keywords = rb_obj_hide(rb_ary_new()); } diff --git a/lib/json/common.rb b/lib/json/common.rb index f796a0c81..6196aef6c 100644 --- a/lib/json/common.rb +++ b/lib/json/common.rb @@ -5,17 +5,10 @@ module JSON module ParserOptions # :nodoc: class << self - def prepare(opts) - if opts[:object_class] || opts[:array_class] - opts = opts.dup - on_load = opts[:on_load] - - on_load = object_class_proc(opts[:object_class], on_load) if opts[:object_class] - on_load = array_class_proc(opts[:array_class], on_load) if opts[:array_class] - opts[:on_load] = on_load - end - - opts + def on_load(on_load, object_class, array_class) + on_load = object_class_proc(object_class, on_load) if object_class + on_load = array_class_proc(array_class, on_load) if array_class + on_load end private @@ -44,6 +37,8 @@ def array_class_proc(array_class, on_load) end end + private_constant :ParserOptions + class << self # :call-seq: # JSON[object] -> new_array or new_string @@ -57,12 +52,14 @@ class << self # ruby = [0, 1, nil] # JSON[ruby] # => '[0,1,null]' def [](object, opts = nil) + opts ||= {}.freeze + if object.is_a?(String) - return JSON.parse(object, opts) + return JSON.parse(object, **opts) elsif object.respond_to?(:to_str) str = object.to_str if str.is_a?(String) - return JSON.parse(str, opts) + return JSON.parse(str, **opts) end end @@ -247,16 +244,14 @@ def to_json(state = nil, *) # # Raises JSON::ParserError (783: unexpected token at ''): # JSON.parse('') # - def parse(source, opts = nil) - opts = ParserOptions.prepare(opts) unless opts.nil? - Parser.parse(source, opts) - end + def parse(source, on_load: nil, object_class: nil, array_class: nil, **options) + if object_class || array_class + on_load = ParserOptions.on_load(on_load, object_class, array_class) + end - PARSE_L_OPTIONS = { - max_nesting: false, - allow_nan: true, - }.freeze - private_constant :PARSE_L_OPTIONS + options[:on_load] = on_load if on_load + Parser.parse(source, options) + end # :call-seq: # JSON.parse!(source, opts) -> object @@ -269,34 +264,30 @@ def parse(source, opts = nil) # - Option +max_nesting+, if not provided, defaults to +false+, # which disables checking for nesting depth. # - Option +allow_nan+, if not provided, defaults to +true+. - def parse!(source, opts = nil) - if opts.nil? - parse(source, PARSE_L_OPTIONS) - else - parse(source, PARSE_L_OPTIONS.merge(opts)) - end + def parse!(source, **options) + parse(source, max_nesting: false, allow_nan: true, **options) end # :call-seq: - # JSON.load_file(path, opts={}) -> object + # JSON.load_file(path, **) -> object # # Calls: - # parse(File.read(path), opts) + # parse(File.read(path), **) # # See method #parse. - def load_file(filespec, opts = nil) - parse(File.read(filespec, encoding: Encoding::UTF_8), opts) + def load_file(filespec, ...) + parse(File.read(filespec, encoding: Encoding::UTF_8), ...) end # :call-seq: - # JSON.load_file!(path, opts = {}) + # JSON.load_file!(path, **) # # Calls: - # JSON.parse!(File.read(path, opts)) + # JSON.parse!(File.read(path), **) # # See method #parse! - def load_file!(filespec, opts = nil) - parse!(File.read(filespec, encoding: Encoding::UTF_8), opts) + def load_file!(filespec, ...) + parse!(File.read(filespec, encoding: Encoding::UTF_8), ...) end # :call-seq: @@ -532,38 +523,8 @@ def pretty_generate(obj, opts = nil) # #"Admin", "password"=>"0wn3d"}>} # - def unsafe_load(source, proc = nil, options = nil) - opts = { - max_nesting: false, - allow_nan: true, - allow_blank: true, - } - if options.nil? && proc && proc.is_a?(Hash) - options, proc = proc, nil - end - - opts.merge!(options) unless options.nil? - - unless source.is_a?(String) - if source.respond_to? :to_str - source = source.to_str - elsif source.respond_to? :to_io - source = source.to_io.read - elsif source.respond_to?(:read) - source = source.read - end - end - - if opts[:allow_blank] && (source.nil? || source.empty?) - source = 'null' - end - - if proc - opts = opts.dup - opts[:on_load] = proc.to_proc - end - - parse(source, opts) + def unsafe_load(source, proc = nil, **options) + load(source, proc, max_nesting: false, **options) end # :call-seq: @@ -692,19 +653,7 @@ def unsafe_load(source, proc = nil, options = nil) # #"Admin", "password"=>"0wn3d"}>} # - def load(source, proc = nil, options = nil) - if proc && options.nil? && proc.is_a?(Hash) - options = proc - proc = nil - end - - opts = { - allow_nan: true, - allow_blank: true, - } - - opts.merge!(options) unless options.nil? - + def load(source, proc = nil, allow_blank: true, **options) unless source.is_a?(String) if source.respond_to? :to_str source = source.to_str @@ -715,16 +664,15 @@ def load(source, proc = nil, options = nil) end end - if opts[:allow_blank] && (source.nil? || (String === source && source.empty?)) + if allow_blank && (source.nil? || (String === source && source.empty?)) source = 'null' end if proc - opts = opts.dup - opts[:on_load] = proc.to_proc + parse(source, allow_nan: true, on_load: proc.to_proc, **options) + else + parse(source, allow_nan: true, **options) end - - parse(source, opts) end # :call-seq: @@ -784,6 +732,20 @@ def dump(obj, anIO = nil, kwargs = nil) # MyApp::JSONC_CODER.load(document) # class Coder + PARSER_OPTIONS = %i( + max_nesting + allow_nan + allow_trailing_comma + allow_comments + allow_control_characters + allow_invalid_escape + symbolize_names + freeze + allow_duplicate_key + decimal_class + ).freeze + private_constant :PARSER_OPTIONS + # :call-seq: # JSON.new(options = nil, &block) # @@ -808,17 +770,19 @@ class Coder # # puts MyApp::API_JSON_CODER.dump(Time.now.utc) # => "2025-01-21T08:41:44.286Z" # - def initialize(options = nil, &as_json) - if options.nil? - options = { strict: true } - else - options = options.dup - options[:strict] = true + def initialize(object_class: nil, array_class: nil, on_load: nil, **options, &as_json) + if object_class || array_class + on_load = ParserOptions.on_load(on_load, object_class, array_class) end - options[:as_json] = as_json if as_json - - @state = State.new(options).freeze - @parser_config = Ext::Parser::Config.new(ParserOptions.prepare(options)).freeze + parser_options = options.slice(*PARSER_OPTIONS) + parser_options[:on_load] = on_load if on_load + @parser_config = Ext::Parser::Config.new(parser_options).freeze + + @state = State.new( + **options, + strict: true, + as_json: as_json, + ).freeze end # call-seq: diff --git a/test/json/json_common_interface_test.rb b/test/json/json_common_interface_test.rb index 9b0802de5..9abe7f731 100644 --- a/test/json/json_common_interface_test.rb +++ b/test/json/json_common_interface_test.rb @@ -58,6 +58,13 @@ def test_parse assert_equal [ 1, 2, 3, ], JSON.parse('[ 1, 2, 3 ]') end + def test_parse_unknown_option + error = assert_raise(ArgumentError) do + JSON.parse('[]', quirks_mode: true) + end + assert_match "quirks_mode", error.message + end + def test_parse_bang assert_equal [ 1, Infinity, 3, ], JSON.parse!('[ 1, Infinity, 3 ]') end From 5a38d268df4962dadd2d17593bbd94f826ff7579 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Mon, 10 Aug 2026 14:24:34 +0200 Subject: [PATCH 14/18] Make generator options strict --- CHANGES.md | 1 + ext/json/ext/generator/generator.c | 12 +++- ext/json/ext/json.h | 13 ++++ ext/json/ext/parser/parser.c | 10 +--- java/src/json/ext/GeneratorState.java | 2 + java/src/json/ext/OptionsReader.java | 30 ++++++++-- java/src/json/ext/Parser.java | 2 + lib/json/common.rb | 13 +++- lib/json/truffle_ruby/generator.rb | 80 +++++++++++-------------- test/json/json_common_interface_test.rb | 17 ++++++ test/json/json_generator_test.rb | 2 +- 11 files changed, 119 insertions(+), 63 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 98893bc68..aed16f405 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ ### Unreleased +* All all unknown options are now cause an `ArgumentError` rather than to be ignored. * The `allow_comments` parsing option now default to `false`. * The `allow_duplicate_key` option now defaults to `false`, for both parsing and generating JSON. * Removed the `limit` positional argument of `JSON.dump`. diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c index 753a5656e..38607cf4e 100644 --- a/ext/json/ext/generator/generator.c +++ b/ext/json/ext/generator/generator.c @@ -1841,6 +1841,7 @@ static VALUE cState_buffer_initial_length_set(VALUE self, VALUE buffer_initial_l struct configure_state_data { JSON_Generator_State *state; VALUE vstate; // Ruby object that owns the state, or Qfalse if stack-allocated + VALUE unknown_keywords; }; static inline void state_write_value(struct configure_state_data *data, VALUE *field, VALUE value) @@ -1878,6 +1879,12 @@ static int configure_state_i(VALUE key, VALUE val, VALUE _arg) else if (key == sym_sort_keys) { state_write_value(data, &state->sort_keys, normalize_sort_keys(val)); } + else { + if (!data->unknown_keywords) { + data->unknown_keywords = rb_obj_hide(rb_ary_new()); + } + rb_ary_push(data->unknown_keywords, key); + } return ST_CONTINUE; } @@ -1891,12 +1898,15 @@ static void configure_state(JSON_Generator_State *state, VALUE vstate, VALUE con struct configure_state_data data = { .state = state, - .vstate = vstate + .vstate = vstate, + .unknown_keywords = Qfalse, }; // We assume in most cases few keys are set so it's faster to go over // the provided keys than to check all possible keys. rb_hash_foreach(config, configure_state_i, (VALUE)&data); + + raise_argument_error_on_unknown_keywords(data.unknown_keywords); } static VALUE cState_configure(VALUE self, VALUE opts) diff --git a/ext/json/ext/json.h b/ext/json/ext/json.h index afddbe2a2..be69bf589 100644 --- a/ext/json/ext/json.h +++ b/ext/json/ext/json.h @@ -180,4 +180,17 @@ static inline VALUE json_rb_catch_obj(VALUE tag, VALUE (*func)(VALUE args), VALU #endif // JSON_TRUFFLERUBY_RB_CATCH_BUG +static inline void raise_argument_error_on_unknown_keywords(VALUE unknown_keywords) +{ + if (RB_UNLIKELY(unknown_keywords)) { + if (RARRAY_LEN(unknown_keywords) == 1) { + rb_raise(rb_eArgError, "unknown keyword: %" PRIsVALUE, RARRAY_AREF(unknown_keywords, 0)); + } + else { + VALUE keywords = rb_ary_join(unknown_keywords, rb_utf8_str_new_cstr(", ")); + rb_raise(rb_eArgError, "unknown keywords: %" PRIsVALUE, keywords); + } + } +} + #endif // _JSON_H_ diff --git a/ext/json/ext/parser/parser.c b/ext/json/ext/parser/parser.c index 8b6c04efe..f10e2260a 100644 --- a/ext/json/ext/parser/parser.c +++ b/ext/json/ext/parser/parser.c @@ -2043,15 +2043,7 @@ static void parser_config_init(JSON_ParserConfig *config, VALUE opts, VALUE self // the provided keys than to check all possible keys. rb_hash_foreach(opts, parser_config_init_i, (VALUE)&args); - if (RB_UNLIKELY(args.unknown_keywords)) { - if (RARRAY_LEN(args.unknown_keywords) == 1) { - rb_raise(rb_eArgError, "unknown keyword: %" PRIsVALUE, RARRAY_AREF(args.unknown_keywords, 0)); - } - else { - VALUE keywords = rb_ary_join(args.unknown_keywords, rb_utf8_str_new_cstr(", ")); - rb_raise(rb_eArgError, "unknown keywords: %" PRIsVALUE, keywords); - } - } + raise_argument_error_on_unknown_keywords(args.unknown_keywords); } /* diff --git a/java/src/json/ext/GeneratorState.java b/java/src/json/ext/GeneratorState.java index 143cf2d7c..dabc9e255 100644 --- a/java/src/json/ext/GeneratorState.java +++ b/java/src/json/ext/GeneratorState.java @@ -577,6 +577,8 @@ public IRubyObject _configure(ThreadContext context, IRubyObject vOpts) { sortKeys = normalizeSortKeys(context, opts.get("sort_keys")); + opts.ensureEmpty(); + return this; } diff --git a/java/src/json/ext/OptionsReader.java b/java/src/json/ext/OptionsReader.java index 829e36b31..d93d3cd79 100644 --- a/java/src/json/ext/OptionsReader.java +++ b/java/src/json/ext/OptionsReader.java @@ -26,15 +26,19 @@ final class OptionsReader { OptionsReader(ThreadContext context, IRubyObject vOpts) { this.context = context; this.runtime = context.runtime; + + RubyHash options; if (vOpts == null || vOpts.isNil()) { - opts = null; + options = null; } else if (vOpts.respondsTo("to_hash")) { - opts = vOpts.convertToHash(); + options = vOpts.convertToHash(); } else if (vOpts.respondsTo("to_h")) { - opts = vOpts.callMethod(context, "to_h").convertToHash(); + options = vOpts.callMethod(context, "to_h").convertToHash(); } else { - opts = vOpts.convertToHash(); /* Should just raise the correct TypeError */ + options = vOpts.convertToHash(); /* Should just raise the correct TypeError */ } + + this.opts = options == null ? null : (RubyHash)options.dup(); } private RuntimeInfo getRuntimeInfo() { @@ -43,6 +47,19 @@ private RuntimeInfo getRuntimeInfo() { return info; } + + void ensureEmpty() { + if (opts == null) return; + + if (opts.size() == 1) { + throw context.runtime.newArgumentError("unknown keyword: " + opts.keys().first()); + } + + if (opts.size() > 1) { + throw context.runtime.newArgumentError("unknown keywords: " + opts.keys().join(context, runtime.newString(", "))); + } + } + /** * Efficiently looks up items with a {@link org.jruby.RubySymbol Symbol} key * @param key The Symbol name to look up for @@ -50,7 +67,10 @@ private RuntimeInfo getRuntimeInfo() { * if not found */ IRubyObject get(String key) { - return opts == null ? null : opts.fastARef(runtime.newSymbol(key)); + if (opts == null) { + return null; + } + return opts.delete(runtime.newSymbol(key)); } boolean hasKey(String key) { diff --git a/java/src/json/ext/Parser.java b/java/src/json/ext/Parser.java index b2be38d15..59ebda179 100644 --- a/java/src/json/ext/Parser.java +++ b/java/src/json/ext/Parser.java @@ -115,6 +115,8 @@ public IRubyObject initialize(ThreadContext context, IRubyObject options) { this.decimalFactory = this::createCustomDecimal; } + opts.ensureEmpty(); + return this; } diff --git a/lib/json/common.rb b/lib/json/common.rb index 6196aef6c..29ac7aa0d 100644 --- a/lib/json/common.rb +++ b/lib/json/common.rb @@ -330,7 +330,7 @@ def generate(obj, opts = nil) if State === opts opts.generate(obj) else - State.generate(obj, opts, nil) + State.generate(obj, opts.frozen? ? opts : opts.dup, nil) end end @@ -387,6 +387,7 @@ def pretty_generate(obj, opts = nil) raise TypeError, "can't convert #{opts.class} into Hash" end end + options = options.merge(opts) end @@ -746,6 +747,13 @@ class Coder ).freeze private_constant :PARSER_OPTIONS + EXCLUDED_GENERATOR_OPTIONS = (PARSER_OPTIONS - %i( + max_nesting + allow_nan + allow_duplicate_key + )).freeze + private_constant :EXCLUDED_GENERATOR_OPTIONS + # :call-seq: # JSON.new(options = nil, &block) # @@ -778,8 +786,9 @@ def initialize(object_class: nil, array_class: nil, on_load: nil, **options, &as parser_options[:on_load] = on_load if on_load @parser_config = Ext::Parser::Config.new(parser_options).freeze + generator_options = options.reject { |k, _| EXCLUDED_GENERATOR_OPTIONS.include?(k) } @state = State.new( - **options, + **generator_options, strict: true, as_json: as_json, ).freeze diff --git a/lib/json/truffle_ruby/generator.rb b/lib/json/truffle_ruby/generator.rb index 4f033ac58..6dad5e7f4 100644 --- a/lib/json/truffle_ruby/generator.rb +++ b/lib/json/truffle_ruby/generator.rb @@ -168,7 +168,7 @@ def initialize(opts = nil) @max_nesting = 100 @sort_keys = false @allow_duplicate_key = false - configure(opts) if opts + _configure(**opts) if opts end # This string is used to indent levels in the JSON text. @@ -290,58 +290,48 @@ def strict? # Configure this State instance with the Hash _opts_, and return # itself. - def configure(opts) - if opts.respond_to?(:to_hash) - opts = opts.to_hash - elsif opts.respond_to?(:to_h) - opts = opts.to_h - else - raise TypeError, "can't convert #{opts.class} into Hash" - end - - if opts[:depth]&.negative? - raise ArgumentError, "depth must be >= 0 (got #{opts[:depth]})" + def configure(options) + unless Hash == options + if options.respond_to?(:to_hash) + options = options.to_hash + elsif options.respond_to?(:to_h) + options = options.to_h + end end + _configure(**options) + end + alias merge configure - opts.each do |key, value| - instance_variable_set "@#{key}", value + private def _configure( + indent: '', space: '', space_before: '', object_nl: '', array_nl: '', allow_nan: false, + as_json: false, ascii_only: false, sort_keys: false, depth: 0, buffer_initial_length: 1024, + allow_duplicate_key: false, script_safe: false, strict: false, max_nesting: 100 + ) + if depth.negative? + raise ArgumentError, "depth must be >= 0 (got #{depth})" end - # NOTE: If adding new instance variables here, check whether #generate should check them for #generate_json - @indent = opts[:indent] || '' if opts.key?(:indent) - @space = opts[:space] || '' if opts.key?(:space) - @space_before = opts[:space_before] || '' if opts.key?(:space_before) - @object_nl = opts[:object_nl] || '' if opts.key?(:object_nl) - @array_nl = opts[:array_nl] || '' if opts.key?(:array_nl) - @allow_nan = !!opts[:allow_nan] if opts.key?(:allow_nan) - @as_json = opts[:as_json].to_proc if opts[:as_json] - @ascii_only = opts[:ascii_only] if opts.key?(:ascii_only) - self.sort_keys = opts[:sort_keys] if opts.key?(:sort_keys) - @depth = opts[:depth] || 0 - @buffer_initial_length ||= opts[:buffer_initial_length] - @allow_duplicate_key = !!opts[:allow_duplicate_key] - - @script_safe = if opts.key?(:script_safe) - !!opts[:script_safe] - else - false + if max_nesting && !(Integer === max_nesting) + raise TypeError, ":max_nesting must be `false` or an Integer, got: #{max_nesting.class}" end - @strict = !!opts[:strict] if opts.key?(:strict) - - if !opts.key?(:max_nesting) # defaults to 100 - @max_nesting = 100 - elsif opts[:max_nesting] - unless opts[:max_nesting].is_a?(Integer) - raise TypeError, ":max_nesting must be an Integer, got: #{opts[:max_nesting].class}" - end - @max_nesting = opts[:max_nesting] - else - @max_nesting = 0 - end + @indent = indent || '' + @space = space || '' + @space_before = space_before || '' + @object_nl = object_nl || '' + @array_nl = array_nl || '' + @allow_nan = allow_nan || false + @as_json = as_json || false + @ascii_only = ascii_only || false + self.sort_keys = sort_keys + @depth = depth + @buffer_initial_length = buffer_initial_length + @allow_duplicate_key = allow_duplicate_key + @script_safe = script_safe + @strict = strict + @max_nesting = max_nesting || 0 self end - alias merge configure def allow_duplicate_key? # :nodoc: @allow_duplicate_key diff --git a/test/json/json_common_interface_test.rb b/test/json/json_common_interface_test.rb index 9abe7f731..1acfc9ba5 100644 --- a/test/json/json_common_interface_test.rb +++ b/test/json/json_common_interface_test.rb @@ -63,6 +63,11 @@ def test_parse_unknown_option JSON.parse('[]', quirks_mode: true) end assert_match "quirks_mode", error.message + + error = assert_raise(ArgumentError) do + JSON.parse('[]', a: 1, b: 2) + end + assert_match "a, b", error.message end def test_parse_bang @@ -73,6 +78,18 @@ def test_generate assert_equal '[1,2,3]', JSON.generate([ 1, 2, 3 ]) end + def test_generate_unknown_option + error = assert_raise(ArgumentError) do + JSON.generate([], quirks_mode: true) + end + assert_match "quirks_mode", error.message + + error = assert_raise(ArgumentError) do + JSON.generate([], a: 1, b: 2) + end + assert_match(/unknown keywords: :?a, :?b/, error.message) + end + def test_fast_generate assert_equal '[1,2,3]', JSON.generate([ 1, 2, 3 ]) end diff --git a/test/json/json_generator_test.rb b/test/json/json_generator_test.rb index 7ae415cf5..27882458a 100755 --- a/test/json/json_generator_test.rb +++ b/test/json/json_generator_test.rb @@ -322,7 +322,7 @@ def test_state_defaults :space => "", :space_before => "", :sort_keys => false, - }.sort_by { |n,| n.to_s }, state.to_h.sort_by { |n,| n.to_s }) + }.sort_by { |n,| n.to_s }.to_h, state.to_h.sort_by { |n,| n.to_s }.to_h) state = JSON::State.new(allow_duplicate_key: true) assert_equal({ From b95bc446a5905e25761022d632e30d28f090c6e6 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Mon, 10 Aug 2026 16:39:28 +0200 Subject: [PATCH 15/18] Add Ractor compatibility tests for JSON.load and JSON.dump --- test/json/ractor_test.rb | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/test/json/ractor_test.rb b/test/json/ractor_test.rb index e53c405a7..9e28f13c1 100644 --- a/test/json/ractor_test.rb +++ b/test/json/ractor_test.rb @@ -19,10 +19,18 @@ module RactorBackport end def test_generate + roundtrip_test(:generate, :parse) + end + + def test_dump + roundtrip_test(:dump, :load) + end + + def roundtrip_test(generator_method, parser_method) pid = fork do Warning[:experimental] = false - r = Ractor.new do - json = JSON.generate({ + generator = Ractor.new(generator_method) do |generator_method| + json = JSON.public_send(generator_method, { 'a' => 2, 'b' => 3.141, 'c' => 'c', @@ -34,10 +42,14 @@ def test_generate }) JSON.parse(json) end - expected_json = JSON.parse('{"a":2,"b":3.141,"c":"c","d":[1,"b",3.14],"e":{"foo":"bar"},' + + + parser = Ractor.new(parser_method) do |parser_method| + JSON.public_send(parser_method, '{"a":2,"b":3.141,"c":"c","d":[1,"b",3.14],"e":{"foo":"bar"},' + '"g":"\\"\\u0000\\u001f","h":1000.0,"i":0.001}') - actual_json = r.value + end + actual_json = generator.value + expected_json = parser.value if expected_json == actual_json exit 0 else From a29668f11e62824db39a6ee3f8638ce975a33283 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Tue, 11 Aug 2026 09:52:45 +0200 Subject: [PATCH 16/18] Get rid of JSON::JSON_LOADED --- lib/json/ext.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/json/ext.rb b/lib/json/ext.rb index 28500f149..da17df1e8 100644 --- a/lib/json/ext.rb +++ b/lib/json/ext.rb @@ -66,6 +66,4 @@ def empty? end end end - - JSON_LOADED = true unless defined?(JSON::JSON_LOADED) end From 6d69dd75d941f723f59ef2b235b1cbae7425c8b3 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Tue, 11 Aug 2026 09:36:47 +0200 Subject: [PATCH 17/18] Prepare 3.x release notes --- CHANGES.md | 22 +++++++++++++++++++++- README.md | 14 +++----------- lib/json.rb | 5 +++-- lib/json/common.rb | 4 ++-- 4 files changed, 29 insertions(+), 16 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index aed16f405..a2a1f5777 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,27 @@ ### Unreleased -* All all unknown options are now cause an `ArgumentError` rather than to be ignored. +With the removal of the insecure `create_additions` option, `JSON.load` and `JSON.dump` are +now safe to use. Them being unsafe by default caused multiple security vulnerabilites in the past. + +If you did depend on `create_additions`, the recommended migration is to [implement a custom serializer using +`JSON::Coder`](https://byroot.github.io/ruby/json/2025/08/02/whats-wrong-with-the-json-gem-api.html#the-create_additions-option). + +All the mutable default options, such as `JSON.load_default_options` have been removed. +They were preventing Ractor compatiblity, and causing bug in libraries using JSON expecting the default behavior. +`JSON` methods now always behave the same unless monkey patched. + +All methods options are now either keyword arguments or checked like keyword arguments, meaning +unknown options such as typos raise `ArgumentError`. + +Duplicated keys are now rejected by default. + +JavaScript comments in documents are no longer supported by default. + +Numerous rarely used aliases have been removed. + +* `JSON.load` defaults are now safe to use. +* All unknown options are now cause an `ArgumentError` rather than to be ignored. * The `allow_comments` parsing option now default to `false`. * The `allow_duplicate_key` option now defaults to `false`, for both parsing and generating JSON. * Removed the `limit` positional argument of `JSON.dump`. diff --git a/README.md b/README.md index 79a8c3f3e..ab550ad7c 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,7 @@ UTF-16 surrogate pairs in order to be able to generate the whole range of unicode code points. All strings, that are to be encoded as JSON strings, should be UTF-8 byte -sequences on the Ruby side. To encode raw binary strings, that aren't UTF-8 -encoded, please use the to\_json\_raw\_object method of String (which produces -an object, that contains a byte array) and decode the result on the receiving -endpoint. +sequences on the Ruby side. ## Installation @@ -49,8 +46,7 @@ JSON.generate(data) ``` You can also use the `pretty_generate` method (which formats the output more -verbosely and nicely) or `fast_generate` (which doesn't do any of the security -checks generate performs, e. g. nesting deepness checks). +verbosely and nicely). ## Casting non native types @@ -148,7 +144,7 @@ JSON.generate({ posts: posts_json, count: posts_json.count }) `JSON.generate` always creates the shortest possible string representation of a ruby data structure in one line. This is good for data storage or network protocols, but not so good for humans to read. Fortunately there's also -`JSON.pretty_generate` (or `JSON.pretty_generate`) that creates a more readable +`JSON.pretty_generate` that creates a more readable output: ```ruby @@ -173,10 +169,6 @@ output: ] ``` -There are also the methods `Kernel#j` for generate, and `Kernel#jj` for -`pretty_generate` output to the console, that work analogous to Core Ruby's `p` and -the `pp` library's `pp` methods. - ## Security When parsing or serializing untrusted input, parser and generator options should never be user controlled. diff --git a/lib/json.rb b/lib/json.rb index 9c77544f1..1e7573df9 100644 --- a/lib/json.rb +++ b/lib/json.rb @@ -374,8 +374,9 @@ # # Raises SystemStackError: stack level too deep # JSON.generate(obj, max_nesting: false) # -# Setting +max_nesting+ to +false+ can lead to a stackoverflow and may leave the program -# in an unrecoverable state. It is discouraged. +# Setting +max_nesting+ to +false+ or a very large number can lead to a stack overflow +# which may leave the process in an unrecoverable state. +# It is highly discouraged. # # ====== Escaping Options # diff --git a/lib/json/common.rb b/lib/json/common.rb index 29ac7aa0d..0512d4aef 100644 --- a/lib/json/common.rb +++ b/lib/json/common.rb @@ -241,8 +241,8 @@ def to_json(state = nil, *) # --- # # Raises an exception if +source+ is not valid JSON: - # # Raises JSON::ParserError (783: unexpected token at ''): - # JSON.parse('') + # # Raises JSON::ParserError unexpected character: 'invalid' at line 1 column 1 : + # JSON.parse('invalid') # def parse(source, on_load: nil, object_class: nil, array_class: nil, **options) if object_class || array_class From c6d71032ee8cf6e539b73e4001176921d47ea9a3 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Tue, 11 Aug 2026 10:00:30 +0200 Subject: [PATCH 18/18] Relase 3.0.0.rc1 --- CHANGES.md | 2 ++ lib/json/version.rb | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index a2a1f5777..b842cb86d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,8 @@ ### Unreleased +### 2026-08-11 (3.0.0.rc1) + With the removal of the insecure `create_additions` option, `JSON.load` and `JSON.dump` are now safe to use. Them being unsafe by default caused multiple security vulnerabilites in the past. diff --git a/lib/json/version.rb b/lib/json/version.rb index 5c66a9d70..8e7e31755 100644 --- a/lib/json/version.rb +++ b/lib/json/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module JSON - VERSION = '2.21.2' + VERSION = '3.0.0.rc1' end