Skip to content

Commit 74e3406

Browse files
authored
Merge pull request #718 from etiennebarrie/json-coder
Introduce JSON::Coder
2 parents edd61b4 + 99040e6 commit 74e3406

File tree

12 files changed

+370
-23
lines changed

12 files changed

+370
-23
lines changed

README.md

+64-3
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ If bundler is not being used to manage dependencies, install the gem by executin
2929

3030
$ gem install json
3131

32-
## Usage
32+
## Basic Usage
3333

3434
To use JSON you can
3535

@@ -52,9 +52,70 @@ You can also use the `pretty_generate` method (which formats the output more
5252
verbosely and nicely) or `fast_generate` (which doesn't do any of the security
5353
checks generate performs, e. g. nesting deepness checks).
5454

55+
## Casting non native types
56+
57+
JSON documents can only support Hashes, Arrays, Strings, Integers and Floats.
58+
59+
By default if you attempt to serialize something else, `JSON.generate` will
60+
search for a `#to_json` method on that object:
61+
62+
```ruby
63+
Position = Struct.new(:latitude, :longitude) do
64+
def to_json(state = nil, *)
65+
JSON::State.from_state(state).generate({
66+
latitude: latitude,
67+
longitude: longitude,
68+
})
69+
end
70+
end
71+
72+
JSON.generate([
73+
Position.new(12323.234, 435345.233),
74+
Position.new(23434.676, 159435.324),
75+
]) # => [{"latitude":12323.234,"longitude":435345.233},{"latitude":23434.676,"longitude":159435.324}]
76+
```
77+
78+
If a `#to_json` method isn't defined on the object, `JSON.generate` will fallback to call `#to_s`:
79+
80+
```ruby
81+
JSON.generate(Object.new) # => "#<Object:0x000000011e768b98>"
82+
```
83+
84+
Both of these behavior can be disabled using the `strict: true` option:
85+
86+
```ruby
87+
JSON.generate(Object.new, strict: true) # => Object not allowed in JSON (JSON::GeneratorError)
88+
JSON.generate(Position.new(1, 2)) # => Position not allowed in JSON (JSON::GeneratorError)
89+
```
90+
91+
## JSON::Coder
92+
93+
Since `#to_json` methods are global, it can sometimes be problematic if you need a given type to be
94+
serialized in different ways in different locations.
95+
96+
Instead it is recommended to use the newer `JSON::Coder` API:
97+
98+
```ruby
99+
module MyApp
100+
API_JSON_CODER = JSON::Coder.new do |object|
101+
case object
102+
when Time
103+
object.iso8601(3)
104+
else
105+
object
106+
end
107+
end
108+
end
109+
110+
puts MyApp::API_JSON_CODER.dump(Time.now.utc) # => "2025-01-21T08:41:44.286Z"
111+
```
112+
113+
The provided block is called for all objects that don't have a native JSON equivalent, and
114+
must return a Ruby object that has a native JSON equivalent.
115+
55116
## Combining JSON fragments
56117

57-
To combine JSON fragments to build a bigger JSON document, you can use `JSON::Fragment`:
118+
To combine JSON fragments into a bigger JSON document, you can use `JSON::Fragment`:
58119

59120
```ruby
60121
posts_json = cache.fetch_multi(post_ids) do |post_id|
@@ -64,7 +125,7 @@ posts_json.map { |post_json| JSON::Fragment.new(post_json) }
64125
JSON.generate({ posts: posts_json, count: posts_json.count })
65126
```
66127

67-
## Handling arbitrary types
128+
## Round-tripping arbitrary types
68129

69130
> [!CAUTION]
70131
> You should never use `JSON.unsafe_load` nor `JSON.parse(str, create_additions: true)` to parse untrusted user input,

benchmark/encoder.rb

+2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717

1818
def implementations(ruby_obj)
1919
state = JSON::State.new(JSON.dump_default_options)
20+
coder = JSON::Coder.new
2021
{
2122
json: ["json", proc { JSON.generate(ruby_obj) }],
23+
json_coder: ["json_coder", proc { coder.dump(ruby_obj) }],
2224
oj: ["oj", proc { Oj.dump(ruby_obj) }],
2325
}
2426
end

benchmark/parser.rb

+2
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@
1515

1616
def benchmark_parsing(name, json_output)
1717
puts "== Parsing #{name} (#{json_output.size} bytes)"
18+
coder = JSON::Coder.new
1819

1920
Benchmark.ips do |x|
2021
x.report("json") { JSON.parse(json_output) } if RUN[:json]
22+
x.report("json_coder") { coder.load(json_output) } if RUN[:json_coder]
2123
x.report("oj") { Oj.load(json_output) } if RUN[:oj]
2224
x.report("Oj::Parser") { Oj::Parser.new(:usual).parse(json_output) } if RUN[:oj]
2325
x.report("rapidjson") { RapidJSON.parse(json_output) } if RUN[:rapidjson]

ext/json/ext/generator/generator.c

+43-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ typedef struct JSON_Generator_StateStruct {
1212
VALUE space_before;
1313
VALUE object_nl;
1414
VALUE array_nl;
15+
VALUE as_json;
1516

1617
long max_nesting;
1718
long depth;
@@ -30,8 +31,8 @@ typedef struct JSON_Generator_StateStruct {
3031
static VALUE mJSON, cState, cFragment, mString_Extend, eGeneratorError, eNestingError, Encoding_UTF_8;
3132

3233
static ID i_to_s, i_to_json, i_new, i_pack, i_unpack, i_create_id, i_extend, i_encode;
33-
static ID sym_indent, sym_space, sym_space_before, sym_object_nl, sym_array_nl, sym_max_nesting, sym_allow_nan,
34-
sym_ascii_only, sym_depth, sym_buffer_initial_length, sym_script_safe, sym_escape_slash, sym_strict;
34+
static VALUE sym_indent, sym_space, sym_space_before, sym_object_nl, sym_array_nl, sym_max_nesting, sym_allow_nan,
35+
sym_ascii_only, sym_depth, sym_buffer_initial_length, sym_script_safe, sym_escape_slash, sym_strict, sym_as_json;
3536

3637

3738
#define GET_STATE_TO(self, state) \
@@ -648,6 +649,7 @@ static void State_mark(void *ptr)
648649
rb_gc_mark_movable(state->space_before);
649650
rb_gc_mark_movable(state->object_nl);
650651
rb_gc_mark_movable(state->array_nl);
652+
rb_gc_mark_movable(state->as_json);
651653
}
652654

653655
static void State_compact(void *ptr)
@@ -658,6 +660,7 @@ static void State_compact(void *ptr)
658660
state->space_before = rb_gc_location(state->space_before);
659661
state->object_nl = rb_gc_location(state->object_nl);
660662
state->array_nl = rb_gc_location(state->array_nl);
663+
state->as_json = rb_gc_location(state->as_json);
661664
}
662665

663666
static void State_free(void *ptr)
@@ -714,6 +717,7 @@ static void vstate_spill(struct generate_json_data *data)
714717
RB_OBJ_WRITTEN(vstate, Qundef, state->space_before);
715718
RB_OBJ_WRITTEN(vstate, Qundef, state->object_nl);
716719
RB_OBJ_WRITTEN(vstate, Qundef, state->array_nl);
720+
RB_OBJ_WRITTEN(vstate, Qundef, state->as_json);
717721
}
718722

719723
static inline VALUE vstate_get(struct generate_json_data *data)
@@ -982,6 +986,8 @@ static void generate_json_fragment(FBuffer *buffer, struct generate_json_data *d
982986
static void generate_json(FBuffer *buffer, struct generate_json_data *data, JSON_Generator_State *state, VALUE obj)
983987
{
984988
VALUE tmp;
989+
bool as_json_called = false;
990+
start:
985991
if (obj == Qnil) {
986992
generate_json_null(buffer, data, state, obj);
987993
} else if (obj == Qfalse) {
@@ -1025,7 +1031,13 @@ static void generate_json(FBuffer *buffer, struct generate_json_data *data, JSON
10251031
default:
10261032
general:
10271033
if (state->strict) {
1028-
raise_generator_error(obj, "%"PRIsVALUE" not allowed in JSON", CLASS_OF(obj));
1034+
if (RTEST(state->as_json) && !as_json_called) {
1035+
obj = rb_proc_call_with_block(state->as_json, 1, &obj, Qnil);
1036+
as_json_called = true;
1037+
goto start;
1038+
} else {
1039+
raise_generator_error(obj, "%"PRIsVALUE" not allowed in JSON", CLASS_OF(obj));
1040+
}
10291041
} else if (rb_respond_to(obj, i_to_json)) {
10301042
tmp = rb_funcall(obj, i_to_json, 1, vstate_get(data));
10311043
Check_Type(tmp, T_STRING);
@@ -1126,6 +1138,7 @@ static VALUE cState_init_copy(VALUE obj, VALUE orig)
11261138
objState->space_before = origState->space_before;
11271139
objState->object_nl = origState->object_nl;
11281140
objState->array_nl = origState->array_nl;
1141+
objState->as_json = origState->as_json;
11291142
return obj;
11301143
}
11311144

@@ -1277,6 +1290,28 @@ static VALUE cState_array_nl_set(VALUE self, VALUE array_nl)
12771290
return Qnil;
12781291
}
12791292

1293+
/*
1294+
* call-seq: as_json()
1295+
*
1296+
* This string is put at the end of a line that holds a JSON array.
1297+
*/
1298+
static VALUE cState_as_json(VALUE self)
1299+
{
1300+
GET_STATE(self);
1301+
return state->as_json;
1302+
}
1303+
1304+
/*
1305+
* call-seq: as_json=(as_json)
1306+
*
1307+
* This string is put at the end of a line that holds a JSON array.
1308+
*/
1309+
static VALUE cState_as_json_set(VALUE self, VALUE as_json)
1310+
{
1311+
GET_STATE(self);
1312+
RB_OBJ_WRITE(self, &state->as_json, rb_convert_type(as_json, T_DATA, "Proc", "to_proc"));
1313+
return Qnil;
1314+
}
12801315

12811316
/*
12821317
* call-seq: check_circular?
@@ -1498,6 +1533,7 @@ static int configure_state_i(VALUE key, VALUE val, VALUE _arg)
14981533
else if (key == sym_script_safe) { state->script_safe = RTEST(val); }
14991534
else if (key == sym_escape_slash) { state->script_safe = RTEST(val); }
15001535
else if (key == sym_strict) { state->strict = RTEST(val); }
1536+
else if (key == sym_as_json) { state->as_json = rb_convert_type(val, T_DATA, "Proc", "to_proc"); }
15011537
return ST_CONTINUE;
15021538
}
15031539

@@ -1589,6 +1625,8 @@ void Init_generator(void)
15891625
rb_define_method(cState, "object_nl=", cState_object_nl_set, 1);
15901626
rb_define_method(cState, "array_nl", cState_array_nl, 0);
15911627
rb_define_method(cState, "array_nl=", cState_array_nl_set, 1);
1628+
rb_define_method(cState, "as_json", cState_as_json, 0);
1629+
rb_define_method(cState, "as_json=", cState_as_json_set, 1);
15921630
rb_define_method(cState, "max_nesting", cState_max_nesting, 0);
15931631
rb_define_method(cState, "max_nesting=", cState_max_nesting_set, 1);
15941632
rb_define_method(cState, "script_safe", cState_script_safe, 0);
@@ -1610,6 +1648,7 @@ void Init_generator(void)
16101648
rb_define_method(cState, "buffer_initial_length", cState_buffer_initial_length, 0);
16111649
rb_define_method(cState, "buffer_initial_length=", cState_buffer_initial_length_set, 1);
16121650
rb_define_method(cState, "generate", cState_generate, -1);
1651+
rb_define_alias(cState, "generate_new", "generate"); // :nodoc:
16131652

16141653
rb_define_singleton_method(cState, "generate", cState_m_generate, 3);
16151654

@@ -1680,6 +1719,7 @@ void Init_generator(void)
16801719
sym_script_safe = ID2SYM(rb_intern("script_safe"));
16811720
sym_escape_slash = ID2SYM(rb_intern("escape_slash"));
16821721
sym_strict = ID2SYM(rb_intern("strict"));
1722+
sym_as_json = ID2SYM(rb_intern("as_json"));
16831723

16841724
usascii_encindex = rb_usascii_encindex();
16851725
utf8_encindex = rb_utf8_encindex();

java/src/json/ext/Generator.java

+37
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
*/
66
package json.ext;
77

8+
import json.ext.RuntimeInfo;
9+
810
import org.jcodings.Encoding;
911
import org.jcodings.specific.ASCIIEncoding;
1012
import org.jcodings.specific.USASCIIEncoding;
@@ -115,6 +117,11 @@ private static <T extends IRubyObject> Handler<? super T> getHandlerFor(Ruby run
115117
case HASH :
116118
if (Helpers.metaclass(object) != runtime.getHash()) break;
117119
return (Handler<T>) HASH_HANDLER;
120+
case STRUCT :
121+
RuntimeInfo info = RuntimeInfo.forRuntime(runtime);
122+
RubyClass fragmentClass = info.jsonModule.get().getClass("Fragment");
123+
if (Helpers.metaclass(object) != fragmentClass) break;
124+
return (Handler<T>) FRAGMENT_HANDLER;
118125
}
119126
return GENERIC_HANDLER;
120127
}
@@ -481,6 +488,28 @@ static RubyString ensureValidEncoding(ThreadContext context, RubyString str) {
481488
static final Handler<IRubyObject> NIL_HANDLER =
482489
new KeywordHandler<>("null");
483490

491+
/**
492+
* The default handler (<code>Object#to_json</code>): coerces the object
493+
* to string using <code>#to_s</code>, and serializes that string.
494+
*/
495+
static final Handler<IRubyObject> FRAGMENT_HANDLER =
496+
new Handler<IRubyObject>() {
497+
@Override
498+
RubyString generateNew(ThreadContext context, Session session, IRubyObject object) {
499+
GeneratorState state = session.getState(context);
500+
IRubyObject result = object.callMethod(context, "to_json", state);
501+
if (result instanceof RubyString) return (RubyString)result;
502+
throw context.runtime.newTypeError("to_json must return a String");
503+
}
504+
505+
@Override
506+
void generate(ThreadContext context, Session session, IRubyObject object, OutputStream buffer) throws IOException {
507+
RubyString result = generateNew(context, session, object);
508+
ByteList bytes = result.getByteList();
509+
buffer.write(bytes.unsafeBytes(), bytes.begin(), bytes.length());
510+
}
511+
};
512+
484513
/**
485514
* The default handler (<code>Object#to_json</code>): coerces the object
486515
* to string using <code>#to_s</code>, and serializes that string.
@@ -510,6 +539,14 @@ void generate(ThreadContext context, Session session, IRubyObject object, Output
510539
RubyString generateNew(ThreadContext context, Session session, IRubyObject object) {
511540
GeneratorState state = session.getState(context);
512541
if (state.strict()) {
542+
if (state.getAsJSON() != null ) {
543+
IRubyObject value = state.getAsJSON().call(context, object);
544+
Handler handler = getHandlerFor(context.runtime, value);
545+
if (handler == GENERIC_HANDLER) {
546+
throw Utils.buildGeneratorError(context, object, value + " returned by as_json not allowed in JSON").toThrowable();
547+
}
548+
return handler.generateNew(context, session, value);
549+
}
513550
throw Utils.buildGeneratorError(context, object, object + " not allowed in JSON").toThrowable();
514551
} else if (object.respondsTo("to_json")) {
515552
IRubyObject result = object.callMethod(context, "to_json", state);

0 commit comments

Comments
 (0)