Skip to content
Merged
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
5 changes: 5 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
* Improve `JSON.load` and `JSON.unsafe_load` to allow passing options as second argument.
* Fix the parser to no longer ignore invalid escapes in strings.
Only `\"`, `\\`, `\b`, `\f`, `\n`, `\r`, `\t` and `\u` are valid JSON escapes.
* Fixed `JSON::Coder` to use the depth it was initialized with.
* On TruffleRuby, fix the generator to not call `to_json` on the return value of `as_json` for `Float::NAN`.
* Fixed handling of `state.depth`: when `to_json` changes `state.depth` but does not restore it, it is reset
automatically to its initial value.
In particular, when a `NestingError` is raised, `depth` is no longer equal to `max_nesting` after the call to
generate, and is reset to its initial value. Similarly when `to_json` raises an exception.

### 2025-11-07 (2.16.0)

Expand Down
50 changes: 7 additions & 43 deletions ext/json/ext/generator/generator.c
Original file line number Diff line number Diff line change
Expand Up @@ -968,14 +968,16 @@ static void vstate_spill(struct generate_json_data *data)
RB_OBJ_WRITTEN(vstate, Qundef, state->as_json);
}

static inline VALUE vstate_get(struct generate_json_data *data)
static inline VALUE json_call_to_json(struct generate_json_data *data, VALUE obj)
{
if (RB_UNLIKELY(!data->vstate)) {
vstate_spill(data);
}
GET_STATE(data->vstate);
state->depth = data->depth;
return data->vstate;
VALUE tmp = rb_funcall(obj, i_to_json, 1, data->vstate);
// no need to restore state->depth, vstate is just a temporary State
return tmp;
}

static VALUE
Expand Down Expand Up @@ -1293,9 +1295,7 @@ static void generate_json_fallback(FBuffer *buffer, struct generate_json_data *d
{
VALUE tmp;
if (rb_respond_to(obj, i_to_json)) {
tmp = rb_funcall(obj, i_to_json, 1, vstate_get(data));
GET_STATE(data->vstate);
data->depth = state->depth;
tmp = json_call_to_json(data, obj);
Check_Type(tmp, T_STRING);
fbuffer_append_str(buffer, tmp);
} else {
Expand Down Expand Up @@ -1477,16 +1477,6 @@ static VALUE generate_json_try(VALUE d)
return fbuffer_finalize(data->buffer);
}

// Preserves the deprecated behavior of State#depth being set.
static VALUE generate_json_ensure_deprecated(VALUE d)
{
struct generate_json_data *data = (struct generate_json_data *)d;
fbuffer_free(data->buffer);
data->state->depth = data->depth;

return Qundef;
}

static VALUE generate_json_ensure(VALUE d)
{
struct generate_json_data *data = (struct generate_json_data *)d;
Expand All @@ -1507,13 +1497,13 @@ static VALUE cState_partial_generate(VALUE self, VALUE obj, generator_func func,

struct generate_json_data data = {
.buffer = &buffer,
.vstate = self,
.vstate = Qfalse, // don't use self as it may be frozen and its depth is mutated when calling to_json
.state = state,
.depth = state->depth,
.obj = obj,
.func = func
};
return rb_ensure(generate_json_try, (VALUE)&data, generate_json_ensure_deprecated, (VALUE)&data);
return rb_ensure(generate_json_try, (VALUE)&data, generate_json_ensure, (VALUE)&data);
}

/* call-seq:
Expand All @@ -1532,31 +1522,6 @@ static VALUE cState_generate(int argc, VALUE *argv, VALUE self)
return cState_partial_generate(self, obj, generate_json, io);
}

static VALUE cState_generate_new(int argc, VALUE *argv, VALUE self)
{
rb_check_arity(argc, 1, 2);
VALUE obj = argv[0];
VALUE io = argc > 1 ? argv[1] : Qnil;

GET_STATE(self);

char stack_buffer[FBUFFER_STACK_SIZE];
FBuffer buffer = {
.io = RTEST(io) ? io : Qfalse,
};
fbuffer_stack_init(&buffer, state->buffer_initial_length, stack_buffer, FBUFFER_STACK_SIZE);

struct generate_json_data data = {
.buffer = &buffer,
.vstate = Qfalse,
.state = state,
.depth = state->depth,
.obj = obj,
.func = generate_json
};
return rb_ensure(generate_json_try, (VALUE)&data, generate_json_ensure, (VALUE)&data);
}

static VALUE cState_initialize(int argc, VALUE *argv, VALUE self)
{
rb_warn("The json gem extension was loaded with the stdlib ruby code. You should upgrade rubygems with `gem update --system`");
Expand Down Expand Up @@ -2145,7 +2110,6 @@ void Init_generator(void)
rb_define_method(cState, "buffer_initial_length", cState_buffer_initial_length, 0);
rb_define_method(cState, "buffer_initial_length=", cState_buffer_initial_length_set, 1);
rb_define_method(cState, "generate", cState_generate, -1);
rb_define_method(cState, "generate_new", cState_generate_new, -1); // :nodoc:

rb_define_private_method(cState, "allow_duplicate_key?", cState_allow_duplicate_key_p, 0);

Expand Down
43 changes: 31 additions & 12 deletions java/src/json/ext/Generator.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,13 @@ static <T extends IRubyObject> RubyString generateJson(ThreadContext context, T

static <T extends IRubyObject> RubyString generateJson(ThreadContext context, T object, Handler<? super T> handler, IRubyObject arg0) {
Session session = new Session(arg0);
return handler.generateNew(context, session, object);
GeneratorState state = session.getState(context);
int depth = state.depth;
try {
return handler.generateNew(context, session, object);
} finally {
state.depth = depth;
}
}

/**
Expand All @@ -79,18 +85,25 @@ static <T extends IRubyObject> RubyString generateJson(ThreadContext context, T
generateJson(ThreadContext context, T object,
GeneratorState config, IRubyObject io) {
Session session = new Session(config);
Handler<? super T> handler = getHandlerFor(context.runtime, object);
GeneratorState state = session.getState(context);
int depth = state.depth;

if (io.isNil()) {
return handler.generateNew(context, session, object);
}
try {
Handler<? super T> handler = getHandlerFor(context.runtime, object);

if (io.isNil()) {
return handler.generateNew(context, session, object);
}

BufferedOutputStream buffer =
new BufferedOutputStream(
new PatchedIOOutputStream(io, UTF8Encoding.INSTANCE),
IO_BUFFER_SIZE);
handler.generateToBuffer(context, session, object, buffer);
return io;
BufferedOutputStream buffer =
new BufferedOutputStream(
new PatchedIOOutputStream(io, UTF8Encoding.INSTANCE),
IO_BUFFER_SIZE);
handler.generateToBuffer(context, session, object, buffer);
return io;
} finally {
state.depth = depth;
}
}

/**
Expand Down Expand Up @@ -784,7 +797,13 @@ static RubyString generateGenericNew(ThreadContext context, Session session, IRu
}
throw Utils.buildGeneratorError(context, object, object + " not allowed in JSON").toThrowable();
} else if (object.respondsTo("to_json")) {
IRubyObject result = object.callMethod(context, "to_json", state);
int depth = state.depth;
IRubyObject result;
try {
result = object.callMethod(context, "to_json", state);
} finally {
state.depth = depth;
}
if (result instanceof RubyString) return (RubyString)result;
throw context.runtime.newTypeError("to_json must return a String");
} else {
Expand Down
12 changes: 0 additions & 12 deletions java/src/json/ext/GeneratorState.java
Original file line number Diff line number Diff line change
Expand Up @@ -256,18 +256,6 @@ public IRubyObject generate(ThreadContext context, IRubyObject obj) {
return generate(context, obj, context.nil);
}

@JRubyMethod
public IRubyObject generate_new(ThreadContext context, IRubyObject obj, IRubyObject io) {
GeneratorState newState = (GeneratorState)dup();
return newState.generate(context, obj, io);
}

@JRubyMethod
public IRubyObject generate_new(ThreadContext context, IRubyObject obj) {
GeneratorState newState = (GeneratorState)dup();
return newState.generate(context, obj, context.nil);
}

@JRubyMethod(name="[]")
public IRubyObject op_aref(ThreadContext context, IRubyObject vName) {
String name = vName.asJavaString();
Expand Down
2 changes: 1 addition & 1 deletion lib/json/common.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1074,7 +1074,7 @@ def initialize(options = nil, &as_json)
#
# Serialize the given object into a \JSON document.
def dump(object, io = nil)
@state.generate_new(object, io)
@state.generate(object, io)
end
alias_method :generate, :dump

Expand Down
22 changes: 16 additions & 6 deletions lib/json/truffle_ruby/generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,9 @@ def to_h
# created this method raises a
# GeneratorError exception.
def generate(obj, anIO = nil)
return dup.generate(obj, anIO) if frozen?

depth = @depth
if @indent.empty? and @space.empty? and @space_before.empty? and @object_nl.empty? and @array_nl.empty? and
!@ascii_only and !@script_safe and @max_nesting == 0 and (!@strict || Symbol === obj)
result = generate_json(obj, ''.dup)
Expand All @@ -346,10 +349,8 @@ def generate(obj, anIO = nil)
else
result
end
end

def generate_new(obj, anIO = nil) # :nodoc:
dup.generate(obj, anIO)
ensure
@depth = depth unless frozen?
end

# Handles @allow_nan, @buffer_initial_length, other ivars must be the default value (see above)
Expand Down Expand Up @@ -490,8 +491,11 @@ module Hash
# _depth_ is used to find out nesting depth, to indent accordingly.
def to_json(state = nil, *)
state = State.from_state(state)
depth = state.depth
state.check_max_nesting
json_transform(state)
ensure
state.depth = depth
end

private
Expand Down Expand Up @@ -555,17 +559,19 @@ def json_transform(state)
raise GeneratorError.new("#{value.class} returned by #{state.as_json} not allowed in JSON", value)
end
result << value.to_json(state)
state.depth = depth
else
raise GeneratorError.new("#{value.class} not allowed in JSON", value)
end
elsif value.respond_to?(:to_json)
result << value.to_json(state)
state.depth = depth
else
result << %{"#{String(value)}"}
end
first = false
}
depth = state.depth -= 1
depth -= 1
unless first
result << state.object_nl
result << state.indent * depth if indent
Expand All @@ -582,8 +588,11 @@ module Array
# produced JSON string output further.
def to_json(state = nil, *)
state = State.from_state(state)
depth = state.depth
state.check_max_nesting
json_transform(state)
ensure
state.depth = depth
end

private
Expand Down Expand Up @@ -621,12 +630,13 @@ def json_transform(state)
end
elsif value.respond_to?(:to_json)
result << value.to_json(state)
state.depth = depth
else
result << %{"#{String(value)}"}
end
first = false
}
state.depth = depth -= 1
depth -= 1
result << state.array_nl
result << state.indent * depth if indent
result << ']'
Expand Down
Loading