Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@

### 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.
- 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#[]=`.
- Removed `JSON.unparse`.
- Removed `JSON.fast_generate`.
- Removed `JSON.fast_unparse`.
- Removed `JSON.pretty_unparse`.
- Removed `JSON.restore`.
- Removed `JSON::PRETTY_STATE_PROTOTYPE`.
- Removed the insecure `create_additions` option.
- Removed `JSON::GenericObject`.

### 2026-07-13 (2.21.1)

* Fix a compilation issue on Window and Microsoft Visual C++.
Expand Down
74 changes: 1 addition & 73 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion benchmark/standalone.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 5 additions & 20 deletions ext/json/ext/generator/generator.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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;
}

/*
Expand Down Expand Up @@ -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;
Expand Down
64 changes: 7 additions & 57 deletions ext/json/ext/parser/parser.c
Original file line number Diff line number Diff line change
Expand Up @@ -403,19 +403,13 @@ 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_duplicate_key;
enum deprecatable_action on_comment;
int max_nesting;
bool allow_comments;
bool allow_duplicate_key;
bool allow_nan;
bool allow_trailing_comma;
bool allow_control_characters;
Expand All @@ -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;

Expand Down Expand Up @@ -617,17 +610,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)
{
long line, column;
cursor_position(state, &line, &column);

VALUE 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)
Expand Down Expand Up @@ -766,11 +748,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);
}

Expand Down Expand Up @@ -820,11 +801,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
Expand Down Expand Up @@ -1193,17 +1169,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(
Expand All @@ -1224,23 +1189,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)
Expand Down Expand Up @@ -2006,13 +1957,13 @@ 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); }
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)) {
Expand Down Expand Up @@ -2639,7 +2590,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;
}
Expand Down
9 changes: 2 additions & 7 deletions java/src/json/ext/Generator.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
Expand Down
Loading
Loading