json 3.0.0.rc1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 297c71ea2d3c5ecf3b479b5b3708cb36ff532bd700e05e7eea48c53c0e79d96f
4
- data.tar.gz: ca9386834406f49385b0e6e091f3f9db7ef66ccfe2632b3c87d340d212872d4d
3
+ metadata.gz: b4a457db7bd0eba155736808c796e81741e7ea72438768e183a7b7a795e8de4e
4
+ data.tar.gz: 61bb013e78224c5c91f620d3097986735411befd5139f33eaecb2c3c510ababe
5
5
  SHA512:
6
- metadata.gz: 616c27a9a4ca01fce4bc13dc85da3e38bfbc8c13daa57c56396b77e61dcc215b1a6cf2994e1b7ba3c7bdbacf9c6b21ce7b7a147a057c9968d447ebe2f19039ba
7
- data.tar.gz: 3f79549799744cfcc517fedcd722e1644fe44e8497176b5864f882174b48247e2b5c89e14f68acae527c3ee807908b20dc799daf04071acdf3ce363f235b492c
6
+ metadata.gz: 6e0b4b4a8160235dcef28c7924ecda754a7ae792a714e6b5fbc5baa810fd358129c4340dd1056d31df823bf139f2b42bccb9c2d400c7282079d3dcf3b2f8c2c8
7
+ data.tar.gz: e67782447e1934dcada56b848982e6bb15708997b9eca220f7b631470734e0b14b2b227ac82612b03b8eb6b3d54b7686d4627a6d551d05b8252da40203f07087
data/CHANGES.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  ### Unreleased
4
4
 
5
+ ### 2026-09-07 (3.0.0)
6
+
7
+ * Add `JSON::ParserError#json_path` to locate parse errors in the document as a JSONPath-style string (e.g. `$.foo[0].bar`). For duplicate key errors it points at the duplicated key itself.
8
+ * Fix the parser to also reject lone trailing UTF-16 surrogates (`\uDCxx` with no leading partner), symmetric to the leading-surrogate case. The Java parser already rejected these; this closes the CRuby/JRuby parity gap.
9
+
5
10
  ### 2026-08-11 (3.0.0.rc1)
6
11
 
7
12
  With the removal of the insecure `create_additions` option, `JSON.load` and `JSON.dump` are
@@ -24,8 +29,8 @@ JavaScript comments in documents are no longer supported by default.
24
29
  Numerous rarely used aliases have been removed.
25
30
 
26
31
  * `JSON.load` defaults are now safe to use.
27
- * All unknown options are now cause an `ArgumentError` rather than to be ignored.
28
- * The `allow_comments` parsing option now default to `false`.
32
+ * All unknown options will now cause an `ArgumentError` rather than to be ignored.
33
+ * The `allow_comments` parsing option now defaults to `false`.
29
34
  * The `allow_duplicate_key` option now defaults to `false`, for both parsing and generating JSON.
30
35
  * Removed the `limit` positional argument of `JSON.dump`.
31
36
  * Removed the `escape_slash` alias of `script_safe`.
@@ -1124,15 +1124,6 @@ static void generate_json_fallback(FBuffer *buffer, struct generate_json_data *d
1124
1124
  }
1125
1125
  }
1126
1126
 
1127
- static inline void generate_json_symbol(FBuffer *buffer, struct generate_json_data *data, VALUE obj)
1128
- {
1129
- if (data->state->strict) {
1130
- generate_json_string(buffer, data, rb_sym2str(obj));
1131
- } else {
1132
- generate_json_fallback(buffer, data, obj);
1133
- }
1134
- }
1135
-
1136
1127
  static void generate_json_null(FBuffer *buffer, struct generate_json_data *data, VALUE obj)
1137
1128
  {
1138
1129
  fbuffer_append(buffer, "null", 4);
@@ -1218,7 +1209,13 @@ start:
1218
1209
  } else if (RB_FLONUM_P(obj)) {
1219
1210
  generate_json_float(buffer, data, obj);
1220
1211
  } else if (RB_STATIC_SYM_P(obj)) {
1221
- generate_json_symbol(buffer, data, obj);
1212
+ if (data->state->strict) {
1213
+ obj = rb_sym2str(obj);
1214
+ JSON_ASSERT(RBASIC_CLASS(obj) == rb_cString);
1215
+ goto generate_string;
1216
+ }
1217
+
1218
+ generate_json_fallback(buffer, data, obj);
1222
1219
  } else {
1223
1220
  goto general;
1224
1221
  }
@@ -1239,6 +1236,7 @@ start:
1239
1236
  case T_STRING:
1240
1237
  if (fallback && klass != rb_cString) goto general;
1241
1238
 
1239
+ generate_string:
1242
1240
  if (RB_LIKELY(valid_json_string_p(obj))) {
1243
1241
  raw_generate_json_string(buffer, data, obj);
1244
1242
  } else if (as_json_called) {
@@ -1250,7 +1248,13 @@ start:
1250
1248
  }
1251
1249
  break;
1252
1250
  case T_SYMBOL:
1253
- generate_json_symbol(buffer, data, obj);
1251
+ if (data->state->strict) {
1252
+ obj = rb_sym2str(obj);
1253
+ JSON_ASSERT(RBASIC_CLASS(obj) == rb_cString);
1254
+ goto generate_string;
1255
+ }
1256
+
1257
+ generate_json_fallback(buffer, data, obj);
1254
1258
  break;
1255
1259
  case T_FLOAT:
1256
1260
  if (fallback && klass != rb_cFloat) goto general;
@@ -5,7 +5,7 @@
5
5
  static VALUE mJSON, eNestingError, eParserError, Encoding_UTF_8;
6
6
  static VALUE CNaN, CInfinity, CMinusInfinity, JSON_empty_string;
7
7
 
8
- static ID i_new, i_try_convert, i_encode, i_at_line, i_at_column;
8
+ static ID i_new, i_try_convert, i_encode, i_at_line, i_at_column, i_at_json_path;
9
9
  #ifndef HAVE_RB_STR_TO_INTERNED_STR
10
10
  static ID i_uminus;
11
11
  #endif
@@ -651,11 +651,42 @@ static VALUE build_parse_error_message(const char *format, JSON_ParserState *sta
651
651
  return rb_enc_sprintf(enc_utf8, format, ptr);
652
652
  }
653
653
 
654
+ static VALUE json_path_new(JSON_ParserState *state, VALUE duplicate_key)
655
+ {
656
+ VALUE path = rb_ary_new_capa(state->current_nesting);
657
+
658
+ json_frame_stack *frames = state->frames;
659
+ rvalue_stack *values = state->value_stack;
660
+
661
+ for (long depth = 1; depth < frames->head; depth++) {
662
+ json_frame *frame = &frames->ptr[depth];
663
+
664
+ bool innermost = depth == frames->head - 1;
665
+ long child_head = innermost ? values->head : frames->ptr[depth + 1].value_stack_head;
666
+ long count = child_head - frame->value_stack_head;
667
+
668
+ if (frame->type == JSON_FRAME_ARRAY) {
669
+ rb_ary_push(path, LONG2NUM(frame->phase == JSON_PHASE_ARRAY_COMMA ? count - 1 : count));
670
+ } else if (innermost && !UNDEF_P(duplicate_key)) {
671
+ rb_ary_push(path, duplicate_key);
672
+ } else if (count & 1) {
673
+ rb_ary_push(path, values->ptr[child_head - 1]);
674
+ } else if (frame->phase == JSON_PHASE_OBJECT_COMMA && count >= 2) {
675
+ rb_ary_push(path, values->ptr[child_head - 2]);
676
+ } else {
677
+ break;
678
+ }
679
+ }
680
+
681
+ return path;
682
+ }
683
+
654
684
  static VALUE parse_error_new(JSON_ParserState *state, VALUE message, long line, long column, bool eos)
655
685
  {
656
686
  VALUE exc = rb_exc_new_str(eParserError, message);
657
687
  rb_ivar_set(exc, i_at_line, LONG2NUM(line));
658
688
  rb_ivar_set(exc, i_at_column, LONG2NUM(column));
689
+ rb_ivar_set(exc, i_at_json_path, json_path_new(state, Qundef));
659
690
  return exc;
660
691
  }
661
692
 
@@ -1010,6 +1041,9 @@ NOINLINE(static) VALUE json_string_unescape(JSON_ParserState *state, JSON_Parser
1010
1041
  raise_syntax_error_at("incomplete surrogate pair at %s", state, p);
1011
1042
  break;
1012
1043
  }
1044
+ } else if ((ch & 0xFC00) == 0xDC00) {
1045
+ raise_syntax_error_at("unpaired trailing surrogate at %s", state, p);
1046
+ break;
1013
1047
  }
1014
1048
 
1015
1049
  int unescape_len = convert_UTF32_to_UTF8(buffer, ch);
@@ -1199,14 +1233,17 @@ NORETURN(static) void raise_duplicate_key_error(JSON_ParserState *state, VALUE d
1199
1233
  );
1200
1234
 
1201
1235
  rb_str_concat(message, build_parse_error_message("", state));
1236
+ VALUE exc;
1202
1237
  if (state->parser) { // line and columns can't be accurate in resumable
1203
- rb_exc_raise(parse_error_new(state, message, 0, 0, false));
1238
+ exc = parse_error_new(state, message, 0, 0, false);
1204
1239
  } else {
1205
1240
  long line, column;
1206
1241
  cursor_position(state, &line, &column);
1207
1242
  rb_str_catf(message, " at line %ld column %ld", line, column);
1208
- rb_exc_raise(parse_error_new(state, message, line, column, false));
1243
+ exc = parse_error_new(state, message, line, column, false);
1209
1244
  }
1245
+ rb_ivar_set(exc, i_at_json_path, json_path_new(state, duplicate_key));
1246
+ rb_exc_raise(exc);
1210
1247
  }
1211
1248
 
1212
1249
  NOINLINE(static) void json_on_duplicate_key(JSON_ParserState *state, JSON_ParserConfig *config, size_t count, const VALUE *pairs)
@@ -2122,6 +2159,12 @@ static VALUE cParser_parse(JSON_ParserConfig *config, VALUE src)
2122
2159
  // the rvalue stack.
2123
2160
  VALUE result = complete ? *rvalue_stack_peek(state->value_stack, 1) : Qundef;
2124
2161
 
2162
+ if (complete) {
2163
+ json_ensure_eof(state, config);
2164
+ } else {
2165
+ raise_eos_error("unexpected end of input", state);
2166
+ }
2167
+
2125
2168
  // This may be skipped in case of exception, but
2126
2169
  // it won't cause a leak.
2127
2170
  rvalue_stack_eagerly_release(value_stack_handle);
@@ -2130,12 +2173,6 @@ static VALUE cParser_parse(JSON_ParserConfig *config, VALUE src)
2130
2173
  RB_GC_GUARD(frame_stack_handle);
2131
2174
  RB_GC_GUARD(Vsource);
2132
2175
 
2133
- if (complete) {
2134
- json_ensure_eof(state, config);
2135
- } else {
2136
- raise_eos_error("unexpected end of input", state);
2137
- }
2138
-
2139
2176
  return result;
2140
2177
  }
2141
2178
 
@@ -2871,6 +2908,7 @@ void Init_parser(void)
2871
2908
  i_encode = rb_intern("encode");
2872
2909
  i_at_line = rb_intern("@line");
2873
2910
  i_at_column = rb_intern("@column");
2911
+ i_at_json_path = rb_intern("@json_path");
2874
2912
 
2875
2913
  binary_encindex = rb_ascii8bit_encindex();
2876
2914
  utf8_encindex = rb_utf8_encindex();
data/lib/json/common.rb CHANGED
@@ -142,7 +142,56 @@ module JSON
142
142
 
143
143
  # This exception is raised if a parser error occurs.
144
144
  class ParserError < JSONError
145
- attr_reader :line, :column
145
+ # Line number where the parser encountered an error.
146
+ # Is <tt>nil</tt> when raised by JSON::ResumableParser.
147
+ attr_reader :line
148
+
149
+ # Column number where the parser encountered an error.
150
+ # Is <tt>nil</tt> when raised by JSON::ResumableParser.
151
+ attr_reader :column
152
+
153
+ # Returns a best effort JSONPath string representing where in the document
154
+ # the parser encountered an error:
155
+ #
156
+ # begin
157
+ # JSON.parse('{"articles": [ { "title": invalid } ]}')
158
+ # rescue JSON::ParserError => error
159
+ # error.json_path # => "$.articles[0].title"
160
+ # end
161
+ def json_path
162
+ return @json_path if String === @json_path
163
+
164
+ if Array === @json_path
165
+ path = build_json_path(@json_path)
166
+ @json_path = path unless frozen?
167
+ return path
168
+ end
169
+ end
170
+
171
+ private
172
+
173
+ def build_json_path(segments)
174
+ error = false
175
+ path = segments.filter_map do |segment|
176
+ next if error
177
+
178
+ case segment
179
+ when Integer
180
+ "[#{segment}]"
181
+ when String, Symbol
182
+ if segment.match?(/\A[a-zA-Z\$\_][a-zA-Z\$\_0-9]*\z/)
183
+ ".#{segment}"
184
+ else
185
+ segment = segment.to_s.gsub(/["\\]/, { '"' => '\\"', '\\' => '\\\\' })
186
+ %{["#{segment}"]}
187
+ end
188
+ else
189
+ error = true
190
+ nil
191
+ end
192
+ end.join
193
+ "$#{path}".freeze
194
+ end
146
195
  end
147
196
 
148
197
  # This exception is raised if the nesting of parsed data structures is too
data/lib/json/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JSON
4
- VERSION = '3.0.0.rc1'
4
+ VERSION = '3.0.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: json
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0.rc1
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Florian Frank