Skip to content

Commit

Permalink
Add a strict option to Generator
Browse files Browse the repository at this point in the history
The default behavior of calling `to_json` or even `to_s` on unsupported
object is problematic in many cases.

```
>> JSON.parse(JSON.dump(Object.new))
=> "#<Object:0x00000001034d8420>"
```

In some cases like blind serailization into some kind of store,
you may want to assert that `JSON.parse(JSON.dump(obj)) == obj`.

The `strict` option is a way to ensure this by rejecting any type
that doesn't map cleanly to a JSON native type.
  • Loading branch information
byroot committed Feb 20, 2023
1 parent 6447b45 commit f65f228
Show file tree
Hide file tree
Showing 8 changed files with 1,962 additions and 2,974 deletions.
44 changes: 42 additions & 2 deletions ext/json/ext/generator/generator.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ static ID i_to_s, i_to_json, i_new, i_indent, i_space, i_space_before,
i_object_nl, i_array_nl, i_max_nesting, i_allow_nan, i_ascii_only,
i_pack, i_unpack, i_create_id, i_extend, i_key_p,
i_aref, i_send, i_respond_to_p, i_match, i_keys, i_depth,
i_buffer_initial_length, i_dup, i_escape_slash;
i_buffer_initial_length, i_dup, i_escape_slash, i_strict;

/*
* Copyright 2001-2004 Unicode, Inc.
Expand Down Expand Up @@ -728,6 +728,8 @@ static VALUE cState_configure(VALUE self, VALUE opts)
state->ascii_only = RTEST(tmp);
tmp = rb_hash_aref(opts, ID2SYM(i_escape_slash));
state->escape_slash = RTEST(tmp);
tmp = rb_hash_aref(opts, ID2SYM(i_strict));
state->strict = RTEST(tmp);
return self;
}

Expand Down Expand Up @@ -763,6 +765,7 @@ static VALUE cState_to_h(VALUE self)
rb_hash_aset(result, ID2SYM(i_ascii_only), state->ascii_only ? Qtrue : Qfalse);
rb_hash_aset(result, ID2SYM(i_max_nesting), LONG2FIX(state->max_nesting));
rb_hash_aset(result, ID2SYM(i_escape_slash), state->escape_slash ? Qtrue : Qfalse);
rb_hash_aset(result, ID2SYM(i_strict), state->strict ? Qtrue : Qfalse);
rb_hash_aset(result, ID2SYM(i_depth), LONG2FIX(state->depth));
rb_hash_aset(result, ID2SYM(i_buffer_initial_length), LONG2FIX(state->buffer_initial_length));
return result;
Expand Down Expand Up @@ -1028,6 +1031,8 @@ static void generate_json(FBuffer *buffer, VALUE Vstate, JSON_Generator_State *s
generate_json_bignum(buffer, Vstate, state, obj);
} else if (klass == rb_cFloat) {
generate_json_float(buffer, Vstate, state, obj);
} else if (state->strict) {
rb_raise(eGeneratorError, "%"PRIsVALUE" not allowed in JSON", RB_OBJ_STRING(CLASS_OF(obj)));
} else if (rb_respond_to(obj, i_to_json)) {
tmp = rb_funcall(obj, i_to_json, 1, Vstate);
Check_Type(tmp, T_STRING);
Expand Down Expand Up @@ -1402,7 +1407,7 @@ static VALUE cState_escape_slash(VALUE self)
}

/*
* call-seq: escape_slash=(depth)
* call-seq: escape_slash=(enable)
*
* This sets whether or not the forward slashes will be escaped in
* the json output.
Expand All @@ -1414,6 +1419,37 @@ static VALUE cState_escape_slash_set(VALUE self, VALUE enable)
return Qnil;
}

/*
* call-seq: strict
*
* If this boolean is false, types unsupported by the JSON format will
* be serialized as strings.
* If this boolean is true, types unsupported by the JSON format will
* raise a JSON::GeneratorError.
*/
static VALUE cState_strict(VALUE self)
{
GET_STATE(self);
return state->strict ? Qtrue : Qfalse;
}

/*
* call-seq: strict=(enable)
*
* This sets whether or not to serialize types unsupported by the
* JSON format as strings.
* If this boolean is false, types unsupported by the JSON format will
* be serialized as strings.
* If this boolean is true, types unsupported by the JSON format will
* raise a JSON::GeneratorError.
*/
static VALUE cState_strict_set(VALUE self, VALUE enable)
{
GET_STATE(self);
state->strict = RTEST(enable);
return Qnil;
}

/*
* call-seq: allow_nan?
*
Expand Down Expand Up @@ -1533,6 +1569,9 @@ void Init_generator(void)
rb_define_method(cState, "escape_slash", cState_escape_slash, 0);
rb_define_method(cState, "escape_slash?", cState_escape_slash, 0);
rb_define_method(cState, "escape_slash=", cState_escape_slash_set, 1);
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);
rb_define_method(cState, "check_circular?", cState_check_circular_p, 0);
rb_define_method(cState, "allow_nan?", cState_allow_nan_p, 0);
rb_define_method(cState, "ascii_only?", cState_ascii_only_p, 0);
Expand Down Expand Up @@ -1590,6 +1629,7 @@ void Init_generator(void)
i_array_nl = rb_intern("array_nl");
i_max_nesting = rb_intern("max_nesting");
i_escape_slash = rb_intern("escape_slash");
i_strict = rb_intern("strict");
i_allow_nan = rb_intern("allow_nan");
i_ascii_only = rb_intern("ascii_only");
i_depth = rb_intern("depth");
Expand Down
5 changes: 4 additions & 1 deletion ext/json/ext/generator/generator.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ typedef struct JSON_Generator_StateStruct {
char allow_nan;
char ascii_only;
char escape_slash;
char strict;
long depth;
long buffer_initial_length;
} JSON_Generator_State;
Expand Down Expand Up @@ -152,7 +153,9 @@ static VALUE cState_ascii_only_p(VALUE self);
static VALUE cState_depth(VALUE self);
static VALUE cState_depth_set(VALUE self, VALUE depth);
static VALUE cState_escape_slash(VALUE self);
static VALUE cState_escape_slash_set(VALUE self, VALUE depth);
static VALUE cState_escape_slash_set(VALUE self, VALUE escape_slash);
static VALUE cState_strict(VALUE self);
static VALUE cState_strict_set(VALUE self, VALUE strict);
static FBuffer *cState_prepare_buffer(VALUE self);
#ifndef ZALLOC
#define ZALLOC(type) ((type *)ruby_zalloc(sizeof(type)))
Expand Down
Loading

0 comments on commit f65f228

Please sign in to comment.