json 3.0.1-java → 3.1.0-java

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: 6172ab55daf2cd89c12ee78ff4262de865bd7dfd20f40ad8669353b78ebaa8d8
4
- data.tar.gz: c87e0ad55b7637f1d691af7bae9ec9c261622aa49b1c66bd3cb80d5a0e6b9c1b
3
+ metadata.gz: ee1372af8bb19939f969cebf3d301b38b9d03c40e48ed62d9d0eda90eb04692f
4
+ data.tar.gz: b380bcb62bfd92420834b2ce75f6fc1e298be512a15f4a5ac5839e967b51c23f
5
5
  SHA512:
6
- metadata.gz: 26761f2f23036bc7aa235eb016d2fe28eac9a21b5c4efae05a987a7461550f356c9d7435f59b87646cb15b9383e04e42e9211354a9d1e6c7be4847c835b85be6
7
- data.tar.gz: 5f5825feaa01223c314de88082508f6928e5171cb65114d8ed51b2636c546a078fbce9294a6baec61d0058492638c89fe110745add121cf14d87fb1d494eb1f7
6
+ metadata.gz: e9788ac086e362d08433e0027ffc269ec4dd58075165b86b0a4fa4c2da7d9de8583d155bf79b0b4389eaab3b10ff92e3b162b808e3dd413b22b2e3473e67fdd4
7
+ data.tar.gz: 51800b6ce2d20e0916b1244f9d18ab10ba3727e0bb03634f723774280fab00f731b0860dd97952e4cdc81ab02fe613260a29afa6547ff2640434df116fe7f9cd
data/CHANGES.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  ### Unreleased
4
4
 
5
+ ### 2026-10-13 (3.1.0)
6
+
7
+ * Added `rfc8785` generator option to canonicalize the output. Replaces the `json-canonicalization` with numerous correctness fixes.
8
+ * Use the `rb_len_t` type introduced in Ruby 4.1 to allow for proper 64bit support on LP64 platforms (Windows).
9
+ * JRuby: parser errors now include the position (`line`, `column` and the message suffix) as well as `json_path`, matching the C extension.
10
+ * Improved the accuracy of parser error's `line`, `column` and snipets in presence of multibyte characters.
11
+ * Fix `JSON.load` to work correctly when provided a `Zlib::GzipReader` as IO source.
12
+ * Reject non-ASCII BINARY strings during JSON generation. This was deprecated since a long time and scheduled to be removed for 3.0.
13
+
14
+ ### 2026-09-09 (3.0.2)
15
+
16
+ * Fix `JSON.load_file` and `JSON.load_file!` to load on Ruby 2.7.0 through 2.7.2, which cannot parse a leading parameter before `...`.
17
+
5
18
  ### 2026-09-08 (3.0.1)
6
19
 
7
20
  * Restore the `limit` positional argument of `JSON.dump`.
data/lib/json/common.rb CHANGED
@@ -80,14 +80,6 @@ module JSON
80
80
  def generator=(generator) # :nodoc:
81
81
  old, $VERBOSE = $VERBOSE, nil
82
82
 
83
- # The default proc used when the +sort_keys+ generation option is +true+.
84
- # It returns a new hash with the entries sorted by their keys.
85
- sort_keys_proc = ->(hash) { hash.sort.to_h }
86
- if defined?(::Ractor) && Ractor.respond_to?(:shareable_lambda)
87
- sort_keys_proc = Ractor.shareable_lambda(&sort_keys_proc)
88
- end
89
- generator::State.default_sort_keys_proc = sort_keys_proc
90
-
91
83
  @generator = generator
92
84
  if generator.const_defined?(:GeneratorMethods)
93
85
  generator_methods = generator::GeneratorMethods
@@ -143,11 +135,11 @@ module JSON
143
135
  # This exception is raised if a parser error occurs.
144
136
  class ParserError < JSONError
145
137
  # Line number where the parser encountered an error.
146
- # Is <tt>nil</tt> when raised by JSON::ResumableParser.
138
+ # Is +nil+ when raised by JSON::ResumableParser.
147
139
  attr_reader :line
148
140
 
149
- # Column number where the parser encountered an error.
150
- # Is <tt>nil</tt> when raised by JSON::ResumableParser.
141
+ # One-based column number where the parser encountered an error, counted in Unicode codepoints.
142
+ # Is +nil+ when raised by JSON::ResumableParser.
151
143
  attr_reader :column
152
144
 
153
145
  # Returns a best effort JSONPath string representing where in the document
@@ -179,7 +171,10 @@ module JSON
179
171
  when Integer
180
172
  "[#{segment}]"
181
173
  when String, Symbol
182
- if segment.match?(/\A[a-zA-Z\$\_][a-zA-Z\$\_0-9]*\z/)
174
+ if !segment.valid_encoding?
175
+ segment = segment.b.gsub(/["\\]/n, { '"' => '\\"', '\\' => '\\\\' }).force_encoding(Encoding::UTF_8)
176
+ %{["#{segment}"]}
177
+ elsif segment.match?(/\A[a-zA-Z\$\_][a-zA-Z\$\_0-9]*\z/)
183
178
  ".#{segment}"
184
179
  else
185
180
  segment = segment.to_s.gsub(/["\\]/, { '"' => '\\"', '\\' => '\\\\' })
@@ -232,7 +227,7 @@ module JSON
232
227
  Fragment = Struct.new(:json) do
233
228
  def initialize(json)
234
229
  unless string = String.try_convert(json)
235
- raise TypeError, " no implicit conversion of #{json.class} into String"
230
+ raise TypeError, "no implicit conversion of #{json.class} into String"
236
231
  end
237
232
 
238
233
  super(string)
@@ -290,7 +285,7 @@ module JSON
290
285
  # ---
291
286
  #
292
287
  # Raises an exception if +source+ is not valid JSON:
293
- # # Raises JSON::ParserError unexpected character: 'invalid' at line 1 column 1 :
288
+ # # Raises JSON::ParserError (unexpected character: 'invalid' at line 1 column 1):
294
289
  # JSON.parse('invalid')
295
290
  #
296
291
  def parse(source, on_load: nil, object_class: nil, array_class: nil, **options)
@@ -324,8 +319,8 @@ module JSON
324
319
  # parse(File.read(path), **)
325
320
  #
326
321
  # See method #parse.
327
- def load_file(filespec, ...)
328
- parse(File.read(filespec, encoding: Encoding::UTF_8), ...)
322
+ def load_file(filespec, **options)
323
+ parse(File.read(filespec, encoding: Encoding::UTF_8), **options)
329
324
  end
330
325
 
331
326
  # :call-seq:
@@ -335,8 +330,8 @@ module JSON
335
330
  # JSON.parse!(File.read(path), **)
336
331
  #
337
332
  # See method #parse!
338
- def load_file!(filespec, ...)
339
- parse!(File.read(filespec, encoding: Encoding::UTF_8), ...)
333
+ def load_file!(filespec, **options)
334
+ parse!(File.read(filespec, encoding: Encoding::UTF_8), **options)
340
335
  end
341
336
 
342
337
  # :call-seq:
@@ -372,7 +367,7 @@ module JSON
372
367
  #
373
368
  # Raises an exception if +obj+ contains circular references:
374
369
  # a = []; b = []; a.push(b); b.push(a)
375
- # # Raises JSON::NestingError (nesting of 100 is too deep):
370
+ # # Raises JSON::NestingError (nesting of 100 is too deep. Did you try to serialize objects with circular references?):
376
371
  # JSON.generate(a)
377
372
  #
378
373
  def generate(obj, opts = nil)
@@ -707,10 +702,10 @@ module JSON
707
702
  unless source.is_a?(String)
708
703
  if source.respond_to? :to_str
709
704
  source = source.to_str
705
+ elsif source.respond_to? :read
706
+ source = source.read
710
707
  elsif source.respond_to? :to_io
711
708
  source = source.to_io.read
712
- elsif source.respond_to?(:read)
713
- source = source.read
714
709
  end
715
710
  end
716
711
 
@@ -771,7 +766,7 @@ module JSON
771
766
  end
772
767
 
773
768
  opts = {
774
- allow_nan: true,
769
+ allow_nan: !(kwargs && kwargs[:rfc8785]),
775
770
  }
776
771
  opts[:max_nesting] = _deprecated_limit if _deprecated_limit
777
772
  opts.merge!(kwargs) if kwargs
@@ -812,9 +807,9 @@ module JSON
812
807
  private_constant :EXCLUDED_GENERATOR_OPTIONS
813
808
 
814
809
  # :call-seq:
815
- # JSON.new(options = nil, &block)
810
+ # JSON::Coder.new(**options, &block)
816
811
  #
817
- # Argument +options+, if given, contains a \Hash of options for both parsing and generating.
812
+ # Keyword arguments +options+, if given, are options for both parsing and generating.
818
813
  # See {Parsing Options}[rdoc-ref:JSON@Parsing+Options],
819
814
  # and {Generating Options}[rdoc-ref:JSON@Generating+Options].
820
815
  #
@@ -835,7 +830,12 @@ module JSON
835
830
  #
836
831
  # puts MyApp::API_JSON_CODER.dump(Time.now.utc) # => "2025-01-21T08:41:44.286Z"
837
832
  #
838
- def initialize(object_class: nil, array_class: nil, on_load: nil, **options, &as_json)
833
+ # The conversion callback can also be passed as +as_json:+. If both are given, the block takes precedence.
834
+ # Passing +false+ or +nil+ for +strict:+ raises ArgumentError.
835
+ #
836
+ def initialize(object_class: nil, array_class: nil, on_load: nil, strict: true, **options, &as_json)
837
+ raise ArgumentError, "JSON::Coder requires strict mode" unless strict
838
+
839
839
  if object_class || array_class
840
840
  on_load = ParserOptions.on_load(on_load, object_class, array_class)
841
841
  end
@@ -847,7 +847,7 @@ module JSON
847
847
  @state = State.new(
848
848
  **generator_options,
849
849
  strict: true,
850
- as_json: as_json,
850
+ as_json: as_json || generator_options[:as_json],
851
851
  ).freeze
852
852
  end
853
853
 
@@ -54,7 +54,8 @@ module JSON
54
54
  strict: strict?,
55
55
  depth: depth,
56
56
  buffer_initial_length: buffer_initial_length,
57
- sort_keys: sort_keys
57
+ sort_keys: sort_keys,
58
+ rfc8785: rfc8785?,
58
59
  }
59
60
 
60
61
  allow_duplicate_key = allow_duplicate_key?
@@ -62,11 +63,6 @@ module JSON
62
63
  result[:allow_duplicate_key] = allow_duplicate_key
63
64
  end
64
65
 
65
- instance_variables.each do |iv|
66
- iv = iv.to_s[1..-1]
67
- result[iv.to_sym] = self[iv]
68
- end
69
-
70
66
  result
71
67
  end
72
68
 
Binary file
Binary file
data/lib/json/ext.rb CHANGED
@@ -28,17 +28,143 @@ module JSON
28
28
  end
29
29
  end
30
30
 
31
+ class << self
32
+ if defined?(::Ractor) && Ractor.respond_to?(:shareable_lambda)
33
+ def shareable_lambda(block) # :nodoc:
34
+ Ractor.shareable_lambda(&block)
35
+ end
36
+ else
37
+ def shareable_lambda(block) # :nodoc:
38
+ block
39
+ end
40
+ end
41
+
42
+ def valid_encoding?(str) # :nodoc:
43
+ str.ascii_only? || (str.encoding == Encoding::UTF_8 && str.valid_encoding?)
44
+ end
45
+
46
+ def coerce_key(key, as_json) # :nodoc:
47
+ as_json_called = false
48
+
49
+ unless String === key || Symbol === key
50
+ key = as_json.call(key, true)
51
+ as_json_called = true
52
+ end
53
+
54
+ if Symbol === key
55
+ key = key.to_s
56
+ end
57
+
58
+ unless String === key
59
+ raise GeneratorError.new("#{key.class} not allowed as object key in JSON", key)
60
+ end
61
+
62
+ unless valid_encoding?(key)
63
+ if as_json_called
64
+ raise GeneratorError.new("source sequence is illegal/malformed utf-8", key)
65
+ end
66
+
67
+ key = as_json.call(key, true)
68
+ unless String === key
69
+ raise GeneratorError.new("#{key.class} not allowed as object key in JSON", key)
70
+ end
71
+
72
+ unless Ext.valid_encoding?(key)
73
+ raise GeneratorError.new("source sequence is illegal/malformed utf-8", key)
74
+ end
75
+ end
76
+
77
+ key
78
+ end
79
+
80
+ def on_duplicate_key(hash, as_json)
81
+ set = {}
82
+ hash.each_key do |key|
83
+ key = coerce_key(key, as_json)
84
+ if set[key]
85
+ raise GeneratorError.new("detected duplicate key #{key.inspect}", key)
86
+ else
87
+ set[key] = true
88
+ end
89
+ end
90
+ end
91
+ end
92
+
31
93
  require 'json/ext/parser'
32
94
  Ext::Parser::Config = Ext::ParserConfig
33
95
  JSON.parser = Ext::Parser
34
96
 
35
- if RUBY_ENGINE == 'truffleruby'
97
+ generator = if RUBY_ENGINE == 'truffleruby'
36
98
  require 'json/truffle_ruby/generator'
37
- JSON.generator = JSON::TruffleRuby::Generator
99
+ JSON::TruffleRuby::Generator
38
100
  else
39
101
  require 'json/ext/generator'
40
- JSON.generator = Generator
102
+ Generator
41
103
  end
104
+
105
+ # The default proc used when the +sort_keys+ generation option is +true+.
106
+ # It returns a new hash with the entries sorted by their keys.
107
+ generator::State.default_sort_keys_proc = shareable_lambda(->(hash) {
108
+ hash.sort.to_h
109
+ })
110
+
111
+ # Directly lifted from Gregg Kellogg's json-canonicalization
112
+ generator::State.rfc8785_number_formatter_proc = shareable_lambda(->(num) {
113
+ if Integer === num
114
+ if num > Float::MAX || num < -Float::MAX
115
+ raise GeneratorError.new("Integer out of range for RFC 8785", num)
116
+ end
117
+ num = num.to_f
118
+ end
119
+ if num.zero?
120
+ "0"
121
+ else
122
+ if num < 0
123
+ num, sign = -num, '-'
124
+ end
125
+ native_rep = num.to_s
126
+ decimal, exponential = native_rep.split('e')
127
+ exp_val = exponential.to_i
128
+ exponential = exp_val > 0 ? ('+' + exp_val.to_s) : exp_val.to_s
129
+
130
+ integral, fractional = decimal.split('.')
131
+ fractional = fractional.sub(/0+$/, '') # Remove trailing zeros
132
+
133
+ if exp_val > 0 && exp_val < 21
134
+ while exp_val > 0
135
+ integral += fractional.to_s[0] || '0'
136
+ fractional = fractional.to_s[1..-1]
137
+ exp_val -= 1
138
+ end
139
+ exponential = nil
140
+ elsif exp_val == 0
141
+ exponential = nil
142
+ elsif exp_val < 0 && exp_val > -7
143
+ # Small numbers are shown as 0.etc with e-6 as lower limit
144
+ fractional, integral, exponential = integral + fractional.to_s, '0', nil
145
+ fractional = ("0" * (-exp_val - 1)) + fractional
146
+ end
147
+
148
+ fractional = nil if fractional.to_s.empty?
149
+ sign.to_s + integral + (fractional ? ".#{fractional}" : '') + (exponential ? "e#{exponential}" : '')
150
+ end
151
+ })
152
+
153
+ generator::State.rfc8785_sort_keys_proc = shareable_lambda(->(hash, as_json) {
154
+ converted = if as_json
155
+ hash.transform_keys { |k| Ext.coerce_key(k, as_json) }
156
+ else
157
+ hash.transform_keys(&:to_s)
158
+ end
159
+
160
+ if converted.size < hash.size
161
+ Ext.on_duplicate_key(hash, as_json)
162
+ end
163
+
164
+ converted.sort_by { |k,| k.encode(Encoding::UTF_16) }.to_h
165
+ })
166
+
167
+ JSON.generator = generator
42
168
  end
43
169
 
44
170
  if defined?(ResumableParser) # Not yet available on JRuby
@@ -112,6 +112,8 @@ module JSON
112
112
  # while generating a JSON text from a Ruby data structure.
113
113
  class State
114
114
  singleton_class.attr_accessor :default_sort_keys_proc # :nodoc:
115
+ singleton_class.attr_accessor :rfc8785_number_formatter_proc # :nodoc:
116
+ singleton_class.attr_accessor :rfc8785_sort_keys_proc # :nodoc:
115
117
 
116
118
  def self.generate(obj, opts = nil, io = nil)
117
119
  new(opts).generate(obj, io)
@@ -125,7 +127,7 @@ module JSON
125
127
  if opts
126
128
  case
127
129
  when self === opts
128
- return opts
130
+ return opts.validate_rfc8785
129
131
  when opts.respond_to?(:to_hash)
130
132
  return new(opts.to_hash)
131
133
  when opts.respond_to?(:to_h)
@@ -168,6 +170,7 @@ module JSON
168
170
  @max_nesting = 100
169
171
  @sort_keys = false
170
172
  @allow_duplicate_key = false
173
+ @rfc8785 = false
171
174
  _configure(**opts) if opts
172
175
  end
173
176
 
@@ -189,7 +192,16 @@ module JSON
189
192
  attr_accessor :array_nl
190
193
 
191
194
  # This proc converts unsupported types into native JSON types.
192
- attr_accessor :as_json
195
+ attr_reader :as_json
196
+
197
+ def as_json=(value)
198
+ raise FrozenError, "can't modify frozen #{self.class}: #{inspect}" if frozen?
199
+ value = value.to_proc if !(Proc === value) && value.respond_to?(:to_proc)
200
+ unless Proc === value
201
+ raise TypeError, "as_json must be a Proc"
202
+ end
203
+ @as_json = value
204
+ end
193
205
 
194
206
  # This integer returns the maximum level of data structure nesting in
195
207
  # the generated JSON, max_nesting = 0 if no maximum is checked.
@@ -209,6 +221,9 @@ module JSON
209
221
  # desired order.
210
222
  attr_reader :sort_keys
211
223
 
224
+ # Controls whether RFC8785 (canonicalization) mode is enabled. Otherwise returns false.
225
+ attr_writer :rfc8785
226
+
212
227
  def sort_keys=(value) # :nodoc:
213
228
  type_error = false
214
229
  @sort_keys = case value
@@ -288,6 +303,27 @@ module JSON
288
303
  @strict
289
304
  end
290
305
 
306
+ # Returns true, if RFC8785 (canonicalization) mode is enabled. Otherwise returns false.
307
+ def rfc8785?
308
+ @rfc8785
309
+ end
310
+
311
+ def validate_rfc8785 # :nodoc:
312
+ if @rfc8785
313
+ option = if !@indent.empty? then :indent
314
+ elsif !@space.empty? then :space
315
+ elsif !@space_before.empty? then :space_before
316
+ elsif !@object_nl.empty? then :object_nl
317
+ elsif !@array_nl.empty? then :array_nl
318
+ elsif @ascii_only then :ascii_only
319
+ elsif @script_safe then :script_safe
320
+ elsif @allow_nan then :allow_nan
321
+ end
322
+ raise ArgumentError, "#{option} cannot be used with rfc8785" if option
323
+ end
324
+ self
325
+ end
326
+
291
327
  # Configure this State instance with the Hash _opts_, and return
292
328
  # itself.
293
329
  def configure(options)
@@ -302,10 +338,13 @@ module JSON
302
338
  end
303
339
  alias merge configure
304
340
 
341
+ # Defaults are the current values so that #configure only writes what it was given.
305
342
  private def _configure(
306
- indent: '', space: '', space_before: '', object_nl: '', array_nl: '', allow_nan: false,
307
- as_json: false, ascii_only: false, sort_keys: false, depth: 0, buffer_initial_length: 1024,
308
- allow_duplicate_key: false, script_safe: false, strict: false, max_nesting: 100
343
+ indent: @indent, space: @space, space_before: @space_before, object_nl: @object_nl,
344
+ array_nl: @array_nl, allow_nan: @allow_nan, as_json: @as_json, ascii_only: @ascii_only,
345
+ sort_keys: @sort_keys, depth: @depth, buffer_initial_length: @buffer_initial_length,
346
+ allow_duplicate_key: @allow_duplicate_key, script_safe: @script_safe, strict: @strict,
347
+ max_nesting: @max_nesting, rfc8785: @rfc8785
309
348
  )
310
349
  if depth.negative?
311
350
  raise ArgumentError, "depth must be >= 0 (got #{depth})"
@@ -321,7 +360,11 @@ module JSON
321
360
  @object_nl = object_nl || ''
322
361
  @array_nl = array_nl || ''
323
362
  @allow_nan = allow_nan || false
324
- @as_json = as_json || false
363
+ if as_json
364
+ self.as_json = as_json
365
+ else
366
+ @as_json = false
367
+ end
325
368
  @ascii_only = ascii_only || false
326
369
  self.sort_keys = sort_keys
327
370
  @depth = depth
@@ -330,7 +373,9 @@ module JSON
330
373
  @script_safe = script_safe
331
374
  @strict = strict
332
375
  @max_nesting = max_nesting || 0
333
- self
376
+ self.rfc8785 = rfc8785
377
+
378
+ validate_rfc8785
334
379
  end
335
380
 
336
381
  def allow_duplicate_key? # :nodoc:
@@ -340,13 +385,24 @@ module JSON
340
385
  # Returns the configuration instance variables as a hash, that can be
341
386
  # passed to the configure method.
342
387
  def to_h
343
- result = {}
344
- instance_variables.each do |iv|
345
- key = iv.to_s[1..-1]
346
- result[key.to_sym] = instance_variable_get(iv)
347
- end
348
-
349
- result
388
+ {
389
+ indent: @indent,
390
+ space: @space,
391
+ space_before: @space_before,
392
+ object_nl: @object_nl,
393
+ array_nl: @array_nl,
394
+ as_json: @as_json,
395
+ allow_nan: @allow_nan,
396
+ ascii_only: @ascii_only,
397
+ max_nesting: @max_nesting,
398
+ script_safe: @script_safe,
399
+ strict: @strict,
400
+ depth: @depth,
401
+ buffer_initial_length: @buffer_initial_length,
402
+ sort_keys: @sort_keys,
403
+ allow_duplicate_key: @allow_duplicate_key,
404
+ rfc8785: @rfc8785,
405
+ }
350
406
  end
351
407
 
352
408
  alias to_hash to_h
@@ -359,16 +415,14 @@ module JSON
359
415
  return dup.generate(obj, anIO) if frozen?
360
416
 
361
417
  depth = @depth
418
+ validate_rfc8785
362
419
  if @indent.empty? and @space.empty? and @space_before.empty? and @object_nl.empty? and @array_nl.empty? and
363
420
  !@ascii_only and !@script_safe and @max_nesting == 0 and (!@strict || Symbol === obj) and !@sort_keys
364
421
  result = generate_json(obj, ''.dup)
365
422
  else
366
- if @sort_keys
367
- obj = @sort_keys.call(obj)
368
- end
369
-
370
423
  result = obj.to_json(self)
371
424
  end
425
+
372
426
  JSON::TruffleRuby::Generator.valid_utf8?(result) or raise GeneratorError.new(
373
427
  "source sequence #{result.inspect} is illegal/malformed utf-8",
374
428
  obj
@@ -390,6 +444,11 @@ module JSON
390
444
  buf << '{'
391
445
  first = true
392
446
  key_type = nil
447
+ if @rfc8785
448
+ obj = State.rfc8785_sort_keys_proc.call(obj, @strict && @as_json)
449
+ elsif @sort_keys
450
+ obj = @sort_keys.call(obj)
451
+ end
393
452
  obj.each_pair do |k,v|
394
453
  if first
395
454
  key_type = k.class
@@ -431,7 +490,7 @@ module JSON
431
490
  buf << obj.to_json(self)
432
491
  end
433
492
  when Integer
434
- buf << obj.to_s
493
+ buf << (@rfc8785 ? State.rfc8785_number_formatter_proc.call(obj) : obj.to_s)
435
494
  when Symbol
436
495
  if @strict
437
496
  fast_serialize_string(obj.name, buf)
@@ -500,10 +559,12 @@ module JSON
500
559
  def to_json(state = nil, *)
501
560
  state = State.from_state(state)
502
561
  depth = state.depth
503
- state.check_max_nesting
504
- json_transform(state)
505
- ensure
506
- state.depth = depth
562
+ begin
563
+ state.check_max_nesting
564
+ json_transform(state)
565
+ ensure
566
+ state.depth = depth
567
+ end
507
568
  end
508
569
 
509
570
  private
@@ -521,13 +582,21 @@ module JSON
521
582
  first = true
522
583
  key_type = nil
523
584
  indent = !state.object_nl.empty?
524
- each { |key, value|
585
+ hash = self
586
+
587
+ if state.rfc8785?
588
+ hash = State.rfc8785_sort_keys_proc.call(hash, state.strict? && state.as_json)
589
+ elsif state.sort_keys
590
+ hash = state.sort_keys.call(hash)
591
+ end
592
+
593
+ hash.each { |key, value|
525
594
  if first
526
595
  key_type = key.class
527
596
  else
528
597
  if key_type && !state.allow_duplicate_key? && key_type != key.class
529
598
  key_type = nil # stop checking
530
- JSON.send(:on_mixed_keys_hash, self)
599
+ JSON.send(:on_mixed_keys_hash, hash)
531
600
  end
532
601
  result << delim
533
602
  end
@@ -592,10 +661,12 @@ module JSON
592
661
  def to_json(state = nil, *)
593
662
  state = State.from_state(state)
594
663
  depth = state.depth
595
- state.check_max_nesting
596
- json_transform(state)
597
- ensure
598
- state.depth = depth
664
+ begin
665
+ state.check_max_nesting
666
+ json_transform(state)
667
+ ensure
668
+ state.depth = depth
669
+ end
599
670
  end
600
671
 
601
672
  private
@@ -648,13 +719,20 @@ module JSON
648
719
 
649
720
  module Integer
650
721
  # Returns a JSON string representation for this Integer number.
651
- def to_json(*) to_s end
722
+ def to_json(state = nil, *)
723
+ if state && State.from_state(state).rfc8785?
724
+ State.rfc8785_number_formatter_proc.call(self)
725
+ else
726
+ to_s
727
+ end
728
+ end
652
729
  end
653
730
 
654
731
  module Float
655
732
  # Returns a JSON string representation for this Float number.
656
733
  def to_json(state = nil, *args)
657
734
  state = State.from_state(state)
735
+
658
736
  if infinite? || nan?
659
737
  if state.allow_nan?
660
738
  to_s
@@ -676,6 +754,8 @@ module JSON
676
754
  else
677
755
  raise GeneratorError.new("#{self} not allowed in JSON", self)
678
756
  end
757
+ elsif state.rfc8785?
758
+ State.rfc8785_number_formatter_proc.call(self)
679
759
  else
680
760
  to_s
681
761
  end
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.1'
4
+ VERSION = '3.1.0'
5
5
  end
data/lib/json.rb CHANGED
@@ -44,13 +44,13 @@ require 'json/common'
44
44
  #
45
45
  # You can parse a \String containing \JSON data using
46
46
  # either of two methods:
47
- # - <tt>JSON.parse(source, opts)</tt>
48
- # - <tt>JSON.parse!(source, opts)</tt>
47
+ # - <tt>JSON.parse(source, **opts)</tt>
48
+ # - <tt>JSON.parse!(source, **opts)</tt>
49
49
  #
50
50
  # where
51
51
  # - +source+ is a Ruby object.
52
- # - +opts+ is a \Hash object containing options
53
- # that control both input allowed and output formatting.
52
+ # - +opts+ are keyword arguments that control both input
53
+ # allowed and output formatting.
54
54
  #
55
55
  # The difference between the two methods
56
56
  # is that JSON.parse! omits some checks
@@ -102,7 +102,7 @@ require 'json/common'
102
102
  # ruby # => 1.0
103
103
  # ruby.class # => Float
104
104
  # ruby = JSON.parse('2.0e2')
105
- # ruby # => 200
105
+ # ruby # => 200.0
106
106
  # ruby.class # => Float
107
107
  # Boolean:
108
108
  # ruby = JSON.parse('true')
@@ -131,10 +131,10 @@ require 'json/common'
131
131
  # ruby # => [0, [1, [2, [3]]]]
132
132
  # Too deep:
133
133
  # # Raises JSON::NestingError (nesting of 2 is too deep):
134
- # JSON.parse(source, {max_nesting: 1})
134
+ # JSON.parse(source, max_nesting: 1)
135
135
  # Bad value:
136
- # # Raises TypeError (wrong argument type Symbol (expected Fixnum)):
137
- # JSON.parse(source, {max_nesting: :foo})
136
+ # # Raises TypeError (no implicit conversion of Symbol into Integer):
137
+ # JSON.parse(source, max_nesting: :foo)
138
138
  #
139
139
  # ---
140
140
  #
@@ -142,11 +142,11 @@ require 'json/common'
142
142
  # should be ignored or cause an error to be raised:
143
143
  #
144
144
  # When set to +false+, the default:
145
- # JSON.parse('{"a": 1, "a":2}') => duplicate key at line 1 column 1 (JSON::ParserError)
145
+ # JSON.parse('{"a": 1, "a": 2}') # duplicate key "a" at line 1 column 1 (JSON::ParserError)
146
146
  #
147
147
  # When set to +true+:
148
148
  # # The last value is used.
149
- # JSON.parse('{"a": 1, "a":2}', allow_duplicate_key: true) => {"a" => 2}
149
+ # JSON.parse('{"a": 1, "a": 2}', allow_duplicate_key: true) # => {"a" => 2}
150
150
  #
151
151
  # ---
152
152
  #
@@ -155,15 +155,15 @@ require 'json/common'
155
155
  # defaults to +false+.
156
156
  #
157
157
  # With the default, +false+:
158
- # # Raises JSON::ParserError (225: unexpected token at '[NaN]'):
158
+ # # Raises JSON::ParserError (unexpected token 'NaN]' at line 1 column 2):
159
159
  # JSON.parse('[NaN]')
160
- # # Raises JSON::ParserError (232: unexpected token at '[Infinity]'):
160
+ # # Raises JSON::ParserError (unexpected token 'Infinity]' at line 1 column 2):
161
161
  # JSON.parse('[Infinity]')
162
- # # Raises JSON::ParserError (248: unexpected token at '[-Infinity]'):
162
+ # # Raises JSON::ParserError (invalid number: '-Infinity]' at line 1 column 2):
163
163
  # JSON.parse('[-Infinity]')
164
164
  # Allow:
165
165
  # source = '[NaN, Infinity, -Infinity]'
166
- # ruby = JSON.parse(source, {allow_nan: true})
166
+ # ruby = JSON.parse(source, allow_nan: true)
167
167
  # ruby # => [NaN, Infinity, -Infinity]
168
168
  #
169
169
  # ---
@@ -185,10 +185,10 @@ require 'json/common'
185
185
  # defaults to +false+.
186
186
  #
187
187
  # When set to +false+, the default:
188
- # JSON.parse('/* comment */ {"a": 1, "a":2}') # unexpected character: '/' at line 1 column 1 (JSON::ParserError)
188
+ # JSON.parse('/* comment */ {"a": 1, "a": 2}') # unexpected token '/*' at line 1 column 1 (JSON::ParserError)
189
189
  #
190
190
  # When set to +true+, comments are ignored:
191
- # JSON.parse('/* comment */ {"a": 1, "a":2} // more comment') # => {"a" => 2}
191
+ # JSON.parse('/* comment */ {"a": 1} // more comment', allow_comments: true) # => {"a" => 1}
192
192
  #
193
193
  # ---
194
194
  #
@@ -197,7 +197,7 @@ require 'json/common'
197
197
  # defaults to +false+.
198
198
  #
199
199
  # With the default, +false+:
200
- # JSON.parse(%{"Hello\nWorld"}) # invalid ASCII control character in string (JSON::ParserError)
200
+ # JSON.parse(%{"Hello\nWorld"}) # invalid ASCII control character in string: \nWorld" at line 2 column 0 (JSON::ParserError)
201
201
  #
202
202
  # When enabled:
203
203
  # JSON.parse(%{"Hello\nWorld"}, allow_control_characters: true) # => "Hello\nWorld"
@@ -209,7 +209,7 @@ require 'json/common'
209
209
  # defaults to +false+.
210
210
  #
211
211
  # With the default, +false+:
212
- # JSON.parse('"Hell\o"') # invalid escape character in string (JSON::ParserError)
212
+ # JSON.parse('"Hell\o"') # invalid escape character in string: '\o"' at line 1 column 6 (JSON::ParserError)
213
213
  #
214
214
  # When enabled:
215
215
  # JSON.parse('"Hell\o"', allow_invalid_escape: true) # => "Hello"
@@ -228,8 +228,8 @@ require 'json/common'
228
228
  # ruby = JSON.parse(source)
229
229
  # ruby # => {"a"=>"foo", "b"=>1.0, "c"=>true, "d"=>false, "e"=>nil}
230
230
  # Use Symbols:
231
- # ruby = JSON.parse(source, {symbolize_names: true})
232
- # ruby # => {:a=>"foo", :b=>1.0, :c=>true, :d=>false, :e=>nil}
231
+ # ruby = JSON.parse(source, symbolize_names: true)
232
+ # ruby # => {a: "foo", b: 1.0, c: true, d: false, e: nil}
233
233
  #
234
234
  # ---
235
235
  #
@@ -242,7 +242,7 @@ require 'json/common'
242
242
  # ruby = JSON.parse(source)
243
243
  # ruby.class # => Hash
244
244
  # Use class \OpenStruct:
245
- # ruby = JSON.parse(source, {object_class: OpenStruct})
245
+ # ruby = JSON.parse(source, object_class: OpenStruct)
246
246
  # ruby # => #<OpenStruct a="foo", b=1.0, c=true, d=false, e=nil>
247
247
  #
248
248
  # ---
@@ -256,8 +256,8 @@ require 'json/common'
256
256
  # ruby = JSON.parse(source)
257
257
  # ruby.class # => Array
258
258
  # Use class \Set:
259
- # ruby = JSON.parse(source, {array_class: Set})
260
- # ruby # => #<Set: {"foo", 1.0, true, false, nil}>
259
+ # ruby = JSON.parse(source, array_class: Set)
260
+ # ruby # => Set["foo", 1.0, true, false, nil]
261
261
  #
262
262
  # === Generating \JSON
263
263
  #
@@ -319,22 +319,22 @@ require 'json/common'
319
319
  # a \String containing a \JSON string representation of the source:
320
320
  # JSON.generate(:foo) # => '"foo"'
321
321
  # JSON.generate(Complex(0, 0)) # => '"0+0i"'
322
- # JSON.generate(Dir.new('.')) # => '"#<Dir>"'
322
+ # JSON.generate(Dir.new('.')) # => '"#<Dir:0x...>"'
323
323
  #
324
324
  # ==== Generating Options
325
325
  #
326
326
  # ====== Input Options
327
327
  #
328
328
  # Option +allow_nan+ (boolean) specifies whether
329
- # +NaN+, +Infinity+, and <tt>-Infinity</tt> may be generated;
329
+ # +NaN+, +Infinity+, and +-Infinity+ may be generated;
330
330
  # defaults to +false+.
331
331
  #
332
332
  # With the default, +false+:
333
- # # Raises JSON::GeneratorError (920: NaN not allowed in JSON):
333
+ # # Raises JSON::GeneratorError (NaN not allowed in JSON):
334
334
  # JSON.generate(JSON::NaN)
335
- # # Raises JSON::GeneratorError (917: Infinity not allowed in JSON):
335
+ # # Raises JSON::GeneratorError (Infinity not allowed in JSON):
336
336
  # JSON.generate(JSON::Infinity)
337
- # # Raises JSON::GeneratorError (917: -Infinity not allowed in JSON):
337
+ # # Raises JSON::GeneratorError (-Infinity not allowed in JSON):
338
338
  # JSON.generate(JSON::MinusInfinity)
339
339
  #
340
340
  # Allow:
@@ -345,14 +345,14 @@ require 'json/common'
345
345
  #
346
346
  # Option +allow_duplicate_key+ (boolean) specifies whether
347
347
  # hashes with duplicate keys should be allowed or produce an error.
348
- # defaults to emit a deprecation warning.
348
+ # Defaults to +false+, which raises an error.
349
349
  #
350
- # With the default, <tt>false</tt>:
351
- # JSON.generate({ foo: 1, "foo" => 2 })
350
+ # With the default, +false+:
351
+ # JSON.generate({foo: 1, "foo" => 2})
352
352
  # # detected duplicate key "foo" in {foo: 1, "foo" => 2} (JSON::GeneratorError)
353
353
  #
354
- # With <tt>true</tt>
355
- # JSON.generate({ foo: 1, "foo" => 2 }, allow_duplicate_key: true)
354
+ # With +true+:
355
+ # JSON.generate({foo: 1, "foo" => 2}, allow_duplicate_key: true)
356
356
  # # => '{"foo":1,"foo":2}'
357
357
  #
358
358
  # ---
@@ -365,13 +365,13 @@ require 'json/common'
365
365
  # JSON.generate(obj) # => '[[[[[[0]]]]]]'
366
366
  #
367
367
  # Too deep:
368
- # # Raises JSON::NestingError (nesting of 2 is too deep):
368
+ # # Raises JSON::NestingError (nesting of 2 is too deep. Did you try to serialize objects with circular references?):
369
369
  # JSON.generate(obj, max_nesting: 2)
370
370
  #
371
371
  # With +false+:
372
372
  # obj = []
373
373
  # obj[0] = obj
374
- # # Raises SystemStackError: stack level too deep
374
+ # # Raises SystemStackError (stack level too deep):
375
375
  # JSON.generate(obj, max_nesting: false)
376
376
  #
377
377
  # Setting +max_nesting+ to +false+ or a very large number can lead to a stack overflow
@@ -410,10 +410,18 @@ require 'json/common'
410
410
  # inserted before the colon in each \JSON object's pair;
411
411
  # defaults to the empty \String, <tt>''</tt>.
412
412
  # - Option +sort_keys+ (boolean or \Proc) controls whether and how the keys of a
413
- # hash are sorted when generating the output; defaults to <tt>false</tt>.
413
+ # hash are sorted when generating the output; defaults to +false+.
414
414
  # When +true+, keys are sorted lexicographically. When a \Proc, it receives
415
415
  # the entire \Hash and must return a \Hash with its pairs in the desired
416
416
  # order, allowing for arbitrary sort orders.
417
+ # - Option +rfc8785+ (boolean) controls whether the generated JSON will be canonicalized
418
+ # as defined in RFC8785.
419
+ # Integers are converted to Float before formatting and may be rounded.
420
+ # Integers outside the finite Float range raise JSON::GeneratorError.
421
+ # Nonempty +indent+, +space+, +space_before+, +object_nl+, or +array_nl+,
422
+ # and enabled +ascii_only+, +script_safe+, or +allow_nan+ raise ArgumentError.
423
+ # JSON.dump defaults +allow_nan+ to +false+ when +rfc8785+ is enabled.
424
+ # JSON::Fragment contents are inserted as is; the caller must ensure they are canonical.
417
425
  #
418
426
  # In this example, +obj+ is used first to generate the shortest
419
427
  # \JSON data (no whitespace), then again with all formatting options
@@ -439,7 +447,7 @@ require 'json/common'
439
447
  # "foo" : [
440
448
  # "bar",
441
449
  # "baz"
442
- # ],
450
+ # ],
443
451
  # "bat" : {
444
452
  # "bam" : 0,
445
453
  # "bad" : 1
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: json
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.1
4
+ version: 3.1.0
5
5
  platform: java
6
6
  authors:
7
7
  - Daniel Luz
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-09-08 00:00:00.000000000 Z
10
+ date: 2026-10-13 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  description: A JSON implementation as a JRuby extension.
13
13
  email: dev+ruby@mernen.com