summaryrefslogtreecommitdiff
path: root/ext/json/lib
diff options
context:
space:
mode:
Diffstat (limited to 'ext/json/lib')
-rw-r--r--ext/json/lib/json.rb684
-rw-r--r--ext/json/lib/json/add/bigdecimal.rb49
-rw-r--r--ext/json/lib/json/add/complex.rb36
-rw-r--r--ext/json/lib/json/add/core.rb3
-rw-r--r--ext/json/lib/json/add/date.rb34
-rw-r--r--ext/json/lib/json/add/date_time.rb35
-rw-r--r--ext/json/lib/json/add/exception.rb32
-rw-r--r--ext/json/lib/json/add/ostruct.rb41
-rw-r--r--ext/json/lib/json/add/range.rb41
-rw-r--r--ext/json/lib/json/add/rational.rb35
-rw-r--r--ext/json/lib/json/add/regexp.rb34
-rw-r--r--ext/json/lib/json/add/set.rb31
-rw-r--r--ext/json/lib/json/add/string.rb35
-rw-r--r--ext/json/lib/json/add/struct.rb36
-rw-r--r--ext/json/lib/json/add/symbol.rb41
-rw-r--r--ext/json/lib/json/add/time.rb44
-rw-r--r--ext/json/lib/json/common.rb1343
-rw-r--r--ext/json/lib/json/ext.rb40
-rw-r--r--ext/json/lib/json/ext/generator/state.rb103
-rw-r--r--ext/json/lib/json/generic_object.rb18
-rw-r--r--ext/json/lib/json/version.rb10
21 files changed, 2245 insertions, 480 deletions
diff --git a/ext/json/lib/json.rb b/ext/json/lib/json.rb
index b5a6912415..26d601926f 100644
--- a/ext/json/lib/json.rb
+++ b/ext/json/lib/json.rb
@@ -1,63 +1,675 @@
-#frozen_string_literal: false
+# frozen_string_literal: true
require 'json/common'
##
-# = JavaScript Object Notation (JSON)
+# = JavaScript \Object Notation (\JSON)
#
-# JSON is a lightweight data-interchange format. It is easy for us
-# humans to read and write. Plus, equally simple for machines to generate or parse.
-# JSON is completely language agnostic, making it the ideal interchange format.
+# \JSON is a lightweight data-interchange format.
#
-# Built on two universally available structures:
-# 1. A collection of name/value pairs. Often referred to as an _object_, hash table, record, struct, keyed list, or associative array.
-# 2. An ordered list of values. More commonly called an _array_, vector, sequence or list.
+# \JSON is easy for us humans to read and write,
+# and equally simple for machines to read (parse) and write (generate).
#
-# To read more about JSON visit: http://json.org
+# \JSON is language-independent, making it an ideal interchange format
+# for applications in differing programming languages
+# and on differing operating systems.
#
-# == Parsing JSON
+# == \JSON Values
#
-# To parse a JSON string received by another application or generated within
-# your existing application:
+# A \JSON value is one of the following:
+# - Double-quoted text: <tt>"foo"</tt>.
+# - Number: +1+, +1.0+, +2.0e2+.
+# - Boolean: +true+, +false+.
+# - Null: +null+.
+# - \Array: an ordered list of values, enclosed by square brackets:
+# ["foo", 1, 1.0, 2.0e2, true, false, null]
#
+# - \Object: a collection of name/value pairs, enclosed by curly braces;
+# each name is double-quoted text;
+# the values may be any \JSON values:
+# {"a": "foo", "b": 1, "c": 1.0, "d": 2.0e2, "e": true, "f": false, "g": null}
+#
+# A \JSON array or object may contain nested arrays, objects, and scalars
+# to any depth:
+# {"foo": {"bar": 1, "baz": 2}, "bat": [0, 1, 2]}
+# [{"foo": 0, "bar": 1}, ["baz", 2]]
+#
+# == Using \Module \JSON
+#
+# To make module \JSON available in your code, begin with:
# require 'json'
#
-# my_hash = JSON.parse('{"hello": "goodbye"}')
-# puts my_hash["hello"] => "goodbye"
+# All examples here assume that this has been done.
#
-# Notice the extra quotes <tt>''</tt> around the hash notation. Ruby expects
-# the argument to be a string and can't convert objects like a hash or array.
+# === Parsing \JSON
#
-# Ruby converts your string into a hash
+# You can parse a \String containing \JSON data using
+# either of two methods:
+# - <tt>JSON.parse(source, opts)</tt>
+# - <tt>JSON.parse!(source, opts)</tt>
#
-# == Generating JSON
+# where
+# - +source+ is a Ruby object.
+# - +opts+ is a \Hash object containing options
+# that control both input allowed and output formatting.
#
-# Creating a JSON string for communication or serialization is
-# just as simple.
+# The difference between the two methods
+# is that JSON.parse! omits some checks
+# and may not be safe for some +source+ data;
+# use it only for data from trusted sources.
+# Use the safer method JSON.parse for less trusted sources.
#
-# require 'json'
+# ==== Parsing \JSON Arrays
#
-# my_hash = {:hello => "goodbye"}
-# puts JSON.generate(my_hash) => "{\"hello\":\"goodbye\"}"
+# When +source+ is a \JSON array, JSON.parse by default returns a Ruby \Array:
+# json = '["foo", 1, 1.0, 2.0e2, true, false, null]'
+# ruby = JSON.parse(json)
+# ruby # => ["foo", 1, 1.0, 200.0, true, false, nil]
+# ruby.class # => Array
#
-# Or an alternative way:
+# The \JSON array may contain nested arrays, objects, and scalars
+# to any depth:
+# json = '[{"foo": 0, "bar": 1}, ["baz", 2]]'
+# JSON.parse(json) # => [{"foo"=>0, "bar"=>1}, ["baz", 2]]
#
-# require 'json'
-# puts {:hello => "goodbye"}.to_json => "{\"hello\":\"goodbye\"}"
+# ==== Parsing \JSON \Objects
+#
+# When the source is a \JSON object, JSON.parse by default returns a Ruby \Hash:
+# json = '{"a": "foo", "b": 1, "c": 1.0, "d": 2.0e2, "e": true, "f": false, "g": null}'
+# ruby = JSON.parse(json)
+# ruby # => {"a"=>"foo", "b"=>1, "c"=>1.0, "d"=>200.0, "e"=>true, "f"=>false, "g"=>nil}
+# ruby.class # => Hash
+#
+# The \JSON object may contain nested arrays, objects, and scalars
+# to any depth:
+# json = '{"foo": {"bar": 1, "baz": 2}, "bat": [0, 1, 2]}'
+# JSON.parse(json) # => {"foo"=>{"bar"=>1, "baz"=>2}, "bat"=>[0, 1, 2]}
+#
+# ==== Parsing \JSON Scalars
+#
+# When the source is a \JSON scalar (not an array or object),
+# JSON.parse returns a Ruby scalar.
+#
+# \String:
+# ruby = JSON.parse('"foo"')
+# ruby # => 'foo'
+# ruby.class # => String
+# \Integer:
+# ruby = JSON.parse('1')
+# ruby # => 1
+# ruby.class # => Integer
+# \Float:
+# ruby = JSON.parse('1.0')
+# ruby # => 1.0
+# ruby.class # => Float
+# ruby = JSON.parse('2.0e2')
+# ruby # => 200
+# ruby.class # => Float
+# Boolean:
+# ruby = JSON.parse('true')
+# ruby # => true
+# ruby.class # => TrueClass
+# ruby = JSON.parse('false')
+# ruby # => false
+# ruby.class # => FalseClass
+# Null:
+# ruby = JSON.parse('null')
+# ruby # => nil
+# ruby.class # => NilClass
+#
+# ==== Parsing Options
+#
+# ====== Input Options
+#
+# Option +max_nesting+ (\Integer) specifies the maximum nesting depth allowed;
+# defaults to +100+; specify +false+ to disable depth checking.
+#
+# With the default, +false+:
+# source = '[0, [1, [2, [3]]]]'
+# ruby = JSON.parse(source)
+# ruby # => [0, [1, [2, [3]]]]
+# Too deep:
+# # Raises JSON::NestingError (nesting of 2 is too deep):
+# JSON.parse(source, {max_nesting: 1})
+# Bad value:
+# # Raises TypeError (wrong argument type Symbol (expected Fixnum)):
+# JSON.parse(source, {max_nesting: :foo})
+#
+# ---
+#
+# Option +allow_duplicate_key+ specifies whether duplicate keys in objects
+# should be ignored or cause an error to be raised:
+#
+# When not specified:
+# # The last value is used and a deprecation warning emitted.
+# JSON.parse('{"a": 1, "a":2}') => {"a" => 2}
+# # warning: detected duplicate keys in JSON object.
+# # This will raise an error in json 3.0 unless enabled via `allow_duplicate_key: true`
+#
+# When set to `+true+`
+# # The last value is used.
+# JSON.parse('{"a": 1, "a":2}') => {"a" => 2}
+#
+# When set to `+false+`, the future default:
+# JSON.parse('{"a": 1, "a":2}') => duplicate key at line 1 column 1 (JSON::ParserError)
+#
+# ---
+#
+# Option +allow_nan+ (boolean) specifies whether to allow
+# NaN, Infinity, and MinusInfinity in +source+;
+# defaults to +false+.
+#
+# With the default, +false+:
+# # Raises JSON::ParserError (225: unexpected token at '[NaN]'):
+# JSON.parse('[NaN]')
+# # Raises JSON::ParserError (232: unexpected token at '[Infinity]'):
+# JSON.parse('[Infinity]')
+# # Raises JSON::ParserError (248: unexpected token at '[-Infinity]'):
+# JSON.parse('[-Infinity]')
+# Allow:
+# source = '[NaN, Infinity, -Infinity]'
+# ruby = JSON.parse(source, {allow_nan: true})
+# ruby # => [NaN, Infinity, -Infinity]
+#
+# ---
+#
+# Option +allow_trailing_comma+ (boolean) specifies whether to allow
+# trailing commas in objects and arrays;
+# defaults to +false+.
+#
+# With the default, +false+:
+# JSON.parse('[1,]') # unexpected character: ']' at line 1 column 4 (JSON::ParserError)
+#
+# When enabled:
+# JSON.parse('[1,]', allow_trailing_comma: true) # => [1]
+#
+# ---
+#
+# Option +allow_control_characters+ (boolean) specifies whether to allow
+# unescaped ASCII control characters, such as newlines, in strings;
+# defaults to +false+.
+#
+# With the default, +false+:
+# JSON.parse(%{"Hello\nWorld"}) # invalid ASCII control character in string (JSON::ParserError)
+#
+# When enabled:
+# JSON.parse(%{"Hello\nWorld"}, allow_control_characters: true) # => "Hello\nWorld"
+#
+# ---
+#
+# Option +allow_invalid_escape+ (boolean) specifies whether to ignore backslahes that are followed
+# by an invalid escape character in strings;
+# defaults to +false+.
+#
+# With the default, +false+:
+# JSON.parse('"Hell\o"') # invalid escape character in string (JSON::ParserError)
+#
+# When enabled:
+# JSON.parse('"Hell\o"', allow_invalid_escape: true) # => "Hello"
+#
+# ====== Output Options
+#
+# Option +freeze+ (boolean) specifies whether the returned objects will be frozen;
+# defaults to +false+.
+#
+# Option +symbolize_names+ (boolean) specifies whether returned \Hash keys
+# should be Symbols;
+# defaults to +false+ (use Strings).
+#
+# With the default, +false+:
+# source = '{"a": "foo", "b": 1.0, "c": true, "d": false, "e": null}'
+# ruby = JSON.parse(source)
+# ruby # => {"a"=>"foo", "b"=>1.0, "c"=>true, "d"=>false, "e"=>nil}
+# Use Symbols:
+# ruby = JSON.parse(source, {symbolize_names: true})
+# ruby # => {:a=>"foo", :b=>1.0, :c=>true, :d=>false, :e=>nil}
+#
+# ---
+#
+# Option +object_class+ (\Class) specifies the Ruby class to be used
+# for each \JSON object;
+# defaults to \Hash.
+#
+# With the default, \Hash:
+# source = '{"a": "foo", "b": 1.0, "c": true, "d": false, "e": null}'
+# ruby = JSON.parse(source)
+# ruby.class # => Hash
+# Use class \OpenStruct:
+# ruby = JSON.parse(source, {object_class: OpenStruct})
+# ruby # => #<OpenStruct a="foo", b=1.0, c=true, d=false, e=nil>
+#
+# ---
+#
+# Option +array_class+ (\Class) specifies the Ruby class to be used
+# for each \JSON array;
+# defaults to \Array.
+#
+# With the default, \Array:
+# source = '["foo", 1.0, true, false, null]'
+# ruby = JSON.parse(source)
+# ruby.class # => Array
+# Use class \Set:
+# ruby = JSON.parse(source, {array_class: Set})
+# ruby # => #<Set: {"foo", 1.0, true, false, nil}>
+#
+# ---
+#
+# Option +create_additions+ (boolean) specifies whether to use \JSON additions in parsing.
+# See {\JSON Additions}[#module-JSON-label-JSON+Additions].
+#
+# === Generating \JSON
+#
+# To generate a Ruby \String containing \JSON data,
+# use method <tt>JSON.generate(source, opts)</tt>, where
+# - +source+ is a Ruby object.
+# - +opts+ is a \Hash object containing options
+# that control both input allowed and output formatting.
+#
+# ==== Generating \JSON from Arrays
+#
+# When the source is a Ruby \Array, JSON.generate returns
+# a \String containing a \JSON array:
+# ruby = [0, 's', :foo]
+# json = JSON.generate(ruby)
+# json # => '[0,"s","foo"]'
+#
+# The Ruby \Array array may contain nested arrays, hashes, and scalars
+# to any depth:
+# ruby = [0, [1, 2], {foo: 3, bar: 4}]
+# json = JSON.generate(ruby)
+# json # => '[0,[1,2],{"foo":3,"bar":4}]'
+#
+# ==== Generating \JSON from Hashes
+#
+# When the source is a Ruby \Hash, JSON.generate returns
+# a \String containing a \JSON object:
+# ruby = {foo: 0, bar: 's', baz: :bat}
+# json = JSON.generate(ruby)
+# json # => '{"foo":0,"bar":"s","baz":"bat"}'
+#
+# The Ruby \Hash array may contain nested arrays, hashes, and scalars
+# to any depth:
+# ruby = {foo: [0, 1], bar: {baz: 2, bat: 3}, bam: :bad}
+# json = JSON.generate(ruby)
+# json # => '{"foo":[0,1],"bar":{"baz":2,"bat":3},"bam":"bad"}'
+#
+# ==== Generating \JSON from Other Objects
+#
+# When the source is neither an \Array nor a \Hash,
+# the generated \JSON data depends on the class of the source.
+#
+# When the source is a Ruby \Integer or \Float, JSON.generate returns
+# a \String containing a \JSON number:
+# JSON.generate(42) # => '42'
+# JSON.generate(0.42) # => '0.42'
#
-# <tt>JSON.generate</tt> only allows objects or arrays to be converted
-# to JSON syntax. <tt>to_json</tt>, however, accepts many Ruby classes
-# even though it acts only as a method for serialization:
+# When the source is a Ruby \String, JSON.generate returns
+# a \String containing a \JSON string (with double-quotes):
+# JSON.generate('A string') # => '"A string"'
#
+# When the source is +true+, +false+ or +nil+, JSON.generate returns
+# a \String containing the corresponding \JSON token:
+# JSON.generate(true) # => 'true'
+# JSON.generate(false) # => 'false'
+# JSON.generate(nil) # => 'null'
+#
+# When the source is none of the above, JSON.generate returns
+# a \String containing a \JSON string representation of the source:
+# JSON.generate(:foo) # => '"foo"'
+# JSON.generate(Complex(0, 0)) # => '"0+0i"'
+# JSON.generate(Dir.new('.')) # => '"#<Dir>"'
+#
+# ==== Generating Options
+#
+# ====== Input Options
+#
+# Option +allow_nan+ (boolean) specifies whether
+# +NaN+, +Infinity+, and <tt>-Infinity</tt> may be generated;
+# defaults to +false+.
+#
+# With the default, +false+:
+# # Raises JSON::GeneratorError (920: NaN not allowed in JSON):
+# JSON.generate(JSON::NaN)
+# # Raises JSON::GeneratorError (917: Infinity not allowed in JSON):
+# JSON.generate(JSON::Infinity)
+# # Raises JSON::GeneratorError (917: -Infinity not allowed in JSON):
+# JSON.generate(JSON::MinusInfinity)
+#
+# Allow:
+# ruby = [Float::NAN, Float::INFINITY, JSON::NaN, JSON::Infinity, JSON::MinusInfinity]
+# JSON.generate(ruby, allow_nan: true) # => '[NaN,Infinity,NaN,Infinity,-Infinity]'
+#
+# ---
+#
+# Option +allow_duplicate_key+ (boolean) specifies whether
+# hashes with duplicate keys should be allowed or produce an error.
+# defaults to emit a deprecation warning.
+#
+# With the default, (not set):
+# Warning[:deprecated] = true
+# JSON.generate({ foo: 1, "foo" => 2 })
+# # warning: detected duplicate key "foo" in {foo: 1, "foo" => 2}.
+# # This will raise an error in json 3.0 unless enabled via `allow_duplicate_key: true`
+# # => '{"foo":1,"foo":2}'
+#
+# With <tt>false</tt>
+# JSON.generate({ foo: 1, "foo" => 2 }, allow_duplicate_key: false)
+# # detected duplicate key "foo" in {foo: 1, "foo" => 2} (JSON::GeneratorError)
+#
+# In version 3.0, <tt>false</tt> will become the default.
+#
+# ---
+#
+# Option +max_nesting+ (\Integer) specifies the maximum nesting depth
+# in +obj+; defaults to +100+.
+#
+# With the default, +100+:
+# obj = [[[[[[0]]]]]]
+# JSON.generate(obj) # => '[[[[[[0]]]]]]'
+#
+# Too deep:
+# # Raises JSON::NestingError (nesting of 2 is too deep):
+# JSON.generate(obj, max_nesting: 2)
+#
+# ====== Escaping Options
+#
+# Options +script_safe+ (boolean) specifies wether <tt>'\u2028'</tt>, <tt>'\u2029'</tt>
+# and <tt>'/'</tt> should be escaped as to make the JSON object safe to interpolate in script
+# tags.
+#
+# Options +ascii_only+ (boolean) specifies wether all characters outside the ASCII range
+# should be escaped.
+#
+# ====== Output Options
+#
+# The default formatting options generate the most compact
+# \JSON data, all on one line and with no whitespace.
+#
+# You can use these formatting options to generate
+# \JSON data in a more open format, using whitespace.
+# See also JSON.pretty_generate.
+#
+# - Option +array_nl+ (\String) specifies a string (usually a newline)
+# to be inserted after each \JSON array; defaults to the empty \String, <tt>''</tt>.
+# - Option +object_nl+ (\String) specifies a string (usually a newline)
+# to be inserted after each \JSON object; defaults to the empty \String, <tt>''</tt>.
+# - Option +indent+ (\String) specifies the string (usually spaces) to be
+# used for indentation; defaults to the empty \String, <tt>''</tt>;
+# defaults to the empty \String, <tt>''</tt>;
+# has no effect unless options +array_nl+ or +object_nl+ specify newlines.
+# - Option +space+ (\String) specifies a string (usually a space) to be
+# inserted after the colon in each \JSON object's pair;
+# defaults to the empty \String, <tt>''</tt>.
+# - Option +space_before+ (\String) specifies a string (usually a space) to be
+# inserted before the colon in each \JSON object's pair;
+# defaults to the empty \String, <tt>''</tt>.
+#
+# In this example, +obj+ is used first to generate the shortest
+# \JSON data (no whitespace), then again with all formatting options
+# specified:
+#
+# obj = {foo: [:bar, :baz], bat: {bam: 0, bad: 1}}
+# json = JSON.generate(obj)
+# puts 'Compact:', json
+# opts = {
+# array_nl: "\n",
+# object_nl: "\n",
+# indent: ' ',
+# space_before: ' ',
+# space: ' '
+# }
+# puts 'Open:', JSON.generate(obj, opts)
+#
+# Output:
+# Compact:
+# {"foo":["bar","baz"],"bat":{"bam":0,"bad":1}}
+# Open:
+# {
+# "foo" : [
+# "bar",
+# "baz"
+# ],
+# "bat" : {
+# "bam" : 0,
+# "bad" : 1
+# }
+# }
+#
+# == \JSON Additions
+#
+# Note that JSON Additions must only be used with trusted data, and is
+# deprecated.
+#
+# When you "round trip" a non-\String object from Ruby to \JSON and back,
+# you have a new \String, instead of the object you began with:
+# ruby0 = Range.new(0, 2)
+# json = JSON.generate(ruby0)
+# json # => '0..2"'
+# ruby1 = JSON.parse(json)
+# ruby1 # => '0..2'
+# ruby1.class # => String
+#
+# You can use \JSON _additions_ to preserve the original object.
+# The addition is an extension of a ruby class, so that:
+# - \JSON.generate stores more information in the \JSON string.
+# - \JSON.parse, called with option +create_additions+,
+# uses that information to create a proper Ruby object.
+#
+# This example shows a \Range being generated into \JSON
+# and parsed back into Ruby, both without and with
+# the addition for \Range:
+# ruby = Range.new(0, 2)
+# # This passage does not use the addition for Range.
+# json0 = JSON.generate(ruby)
+# ruby0 = JSON.parse(json0)
+# # This passage uses the addition for Range.
+# require 'json/add/range'
+# json1 = JSON.generate(ruby)
+# ruby1 = JSON.parse(json1, create_additions: true)
+# # Make a nice display.
+# display = <<~EOT
+# Generated JSON:
+# Without addition: #{json0} (#{json0.class})
+# With addition: #{json1} (#{json1.class})
+# Parsed JSON:
+# Without addition: #{ruby0.inspect} (#{ruby0.class})
+# With addition: #{ruby1.inspect} (#{ruby1.class})
+# EOT
+# puts display
+#
+# This output shows the different results:
+# Generated JSON:
+# Without addition: "0..2" (String)
+# With addition: {"json_class":"Range","a":[0,2,false]} (String)
+# Parsed JSON:
+# Without addition: "0..2" (String)
+# With addition: 0..2 (Range)
+#
+# The \JSON module includes additions for certain classes.
+# You can also craft custom additions.
+# See {Custom \JSON Additions}[#module-JSON-label-Custom+JSON+Additions].
+#
+# === Built-in Additions
+#
+# The \JSON module includes additions for certain classes.
+# To use an addition, +require+ its source:
+# - BigDecimal: <tt>require 'json/add/bigdecimal'</tt>
+# - Complex: <tt>require 'json/add/complex'</tt>
+# - Date: <tt>require 'json/add/date'</tt>
+# - DateTime: <tt>require 'json/add/date_time'</tt>
+# - Exception: <tt>require 'json/add/exception'</tt>
+# - OpenStruct: <tt>require 'json/add/ostruct'</tt>
+# - Range: <tt>require 'json/add/range'</tt>
+# - Rational: <tt>require 'json/add/rational'</tt>
+# - Regexp: <tt>require 'json/add/regexp'</tt>
+# - Set: <tt>require 'json/add/set'</tt>
+# - Struct: <tt>require 'json/add/struct'</tt>
+# - Symbol: <tt>require 'json/add/symbol'</tt>
+# - Time: <tt>require 'json/add/time'</tt>
+#
+# To reduce punctuation clutter, the examples below
+# show the generated \JSON via +puts+, rather than the usual +inspect+,
+#
+# \BigDecimal:
+# require 'json/add/bigdecimal'
+# ruby0 = BigDecimal(0) # 0.0
+# json = JSON.generate(ruby0) # {"json_class":"BigDecimal","b":"27:0.0"}
+# ruby1 = JSON.parse(json, create_additions: true) # 0.0
+# ruby1.class # => BigDecimal
+#
+# \Complex:
+# require 'json/add/complex'
+# ruby0 = Complex(1+0i) # 1+0i
+# json = JSON.generate(ruby0) # {"json_class":"Complex","r":1,"i":0}
+# ruby1 = JSON.parse(json, create_additions: true) # 1+0i
+# ruby1.class # Complex
+#
+# \Date:
+# require 'json/add/date'
+# ruby0 = Date.today # 2020-05-02
+# json = JSON.generate(ruby0) # {"json_class":"Date","y":2020,"m":5,"d":2,"sg":2299161.0}
+# ruby1 = JSON.parse(json, create_additions: true) # 2020-05-02
+# ruby1.class # Date
+#
+# \DateTime:
+# require 'json/add/date_time'
+# ruby0 = DateTime.now # 2020-05-02T10:38:13-05:00
+# json = JSON.generate(ruby0) # {"json_class":"DateTime","y":2020,"m":5,"d":2,"H":10,"M":38,"S":13,"of":"-5/24","sg":2299161.0}
+# ruby1 = JSON.parse(json, create_additions: true) # 2020-05-02T10:38:13-05:00
+# ruby1.class # DateTime
+#
+# \Exception (and its subclasses including \RuntimeError):
+# require 'json/add/exception'
+# ruby0 = Exception.new('A message') # A message
+# json = JSON.generate(ruby0) # {"json_class":"Exception","m":"A message","b":null}
+# ruby1 = JSON.parse(json, create_additions: true) # A message
+# ruby1.class # Exception
+# ruby0 = RuntimeError.new('Another message') # Another message
+# json = JSON.generate(ruby0) # {"json_class":"RuntimeError","m":"Another message","b":null}
+# ruby1 = JSON.parse(json, create_additions: true) # Another message
+# ruby1.class # RuntimeError
+#
+# \OpenStruct:
+# require 'json/add/ostruct'
+# ruby0 = OpenStruct.new(name: 'Matz', language: 'Ruby') # #<OpenStruct name="Matz", language="Ruby">
+# json = JSON.generate(ruby0) # {"json_class":"OpenStruct","t":{"name":"Matz","language":"Ruby"}}
+# ruby1 = JSON.parse(json, create_additions: true) # #<OpenStruct name="Matz", language="Ruby">
+# ruby1.class # OpenStruct
+#
+# \Range:
+# require 'json/add/range'
+# ruby0 = Range.new(0, 2) # 0..2
+# json = JSON.generate(ruby0) # {"json_class":"Range","a":[0,2,false]}
+# ruby1 = JSON.parse(json, create_additions: true) # 0..2
+# ruby1.class # Range
+#
+# \Rational:
+# require 'json/add/rational'
+# ruby0 = Rational(1, 3) # 1/3
+# json = JSON.generate(ruby0) # {"json_class":"Rational","n":1,"d":3}
+# ruby1 = JSON.parse(json, create_additions: true) # 1/3
+# ruby1.class # Rational
+#
+# \Regexp:
+# require 'json/add/regexp'
+# ruby0 = Regexp.new('foo') # (?-mix:foo)
+# json = JSON.generate(ruby0) # {"json_class":"Regexp","o":0,"s":"foo"}
+# ruby1 = JSON.parse(json, create_additions: true) # (?-mix:foo)
+# ruby1.class # Regexp
+#
+# \Set:
+# require 'json/add/set'
+# ruby0 = Set.new([0, 1, 2]) # #<Set: {0, 1, 2}>
+# json = JSON.generate(ruby0) # {"json_class":"Set","a":[0,1,2]}
+# ruby1 = JSON.parse(json, create_additions: true) # #<Set: {0, 1, 2}>
+# ruby1.class # Set
+#
+# \Struct:
+# require 'json/add/struct'
+# Customer = Struct.new(:name, :address) # Customer
+# ruby0 = Customer.new("Dave", "123 Main") # #<struct Customer name="Dave", address="123 Main">
+# json = JSON.generate(ruby0) # {"json_class":"Customer","v":["Dave","123 Main"]}
+# ruby1 = JSON.parse(json, create_additions: true) # #<struct Customer name="Dave", address="123 Main">
+# ruby1.class # Customer
+#
+# \Symbol:
+# require 'json/add/symbol'
+# ruby0 = :foo # foo
+# json = JSON.generate(ruby0) # {"json_class":"Symbol","s":"foo"}
+# ruby1 = JSON.parse(json, create_additions: true) # foo
+# ruby1.class # Symbol
+#
+# \Time:
+# require 'json/add/time'
+# ruby0 = Time.now # 2020-05-02 11:28:26 -0500
+# json = JSON.generate(ruby0) # {"json_class":"Time","s":1588436906,"n":840560000}
+# ruby1 = JSON.parse(json, create_additions: true) # 2020-05-02 11:28:26 -0500
+# ruby1.class # Time
+#
+#
+# === Custom \JSON Additions
+#
+# In addition to the \JSON additions provided,
+# you can craft \JSON additions of your own,
+# either for Ruby built-in classes or for user-defined classes.
+#
+# Here's a user-defined class +Foo+:
+# class Foo
+# attr_accessor :bar, :baz
+# def initialize(bar, baz)
+# self.bar = bar
+# self.baz = baz
+# end
+# end
+#
+# Here's the \JSON addition for it:
+# # Extend class Foo with JSON addition.
+# class Foo
+# # Serialize Foo object with its class name and arguments
+# def to_json(*args)
+# {
+# JSON.create_id => self.class.name,
+# 'a' => [ bar, baz ]
+# }.to_json(*args)
+# end
+# # Deserialize JSON string by constructing new Foo object with arguments.
+# def self.json_create(object)
+# new(*object['a'])
+# end
+# end
+#
+# Demonstration:
# require 'json'
+# # This Foo object has no custom addition.
+# foo0 = Foo.new(0, 1)
+# json0 = JSON.generate(foo0)
+# obj0 = JSON.parse(json0)
+# # Lood the custom addition.
+# require_relative 'foo_addition'
+# # This foo has the custom addition.
+# foo1 = Foo.new(0, 1)
+# json1 = JSON.generate(foo1)
+# obj1 = JSON.parse(json1, create_additions: true)
+# # Make a nice display.
+# display = <<~EOT
+# Generated JSON:
+# Without custom addition: #{json0} (#{json0.class})
+# With custom addition: #{json1} (#{json1.class})
+# Parsed JSON:
+# Without custom addition: #{obj0.inspect} (#{obj0.class})
+# With custom addition: #{obj1.inspect} (#{obj1.class})
+# EOT
+# puts display
+#
+# Output:
#
-# 1.to_json => "1"
+# Generated JSON:
+# Without custom addition: "#<Foo:0x0000000006534e80>" (String)
+# With custom addition: {"json_class":"Foo","a":[0,1]} (String)
+# Parsed JSON:
+# Without custom addition: "#<Foo:0x0000000006534e80>" (String)
+# With custom addition: #<Foo:0x0000000006473bb8 @bar=0, @baz=1> (Foo)
#
module JSON
require 'json/version'
-
- begin
- require 'json/ext'
- rescue LoadError
- require 'json/pure'
- end
+ require 'json/ext'
end
diff --git a/ext/json/lib/json/add/bigdecimal.rb b/ext/json/lib/json/add/bigdecimal.rb
index c8b4f567cb..dc84572f31 100644
--- a/ext/json/lib/json/add/bigdecimal.rb
+++ b/ext/json/lib/json/add/bigdecimal.rb
@@ -1,29 +1,58 @@
-#frozen_string_literal: false
+# frozen_string_literal: true
unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
require 'json'
end
-defined?(::BigDecimal) or require 'bigdecimal'
+begin
+ require 'bigdecimal'
+rescue LoadError
+end
class BigDecimal
- # Import a JSON Marshalled object.
- #
- # method used for JSON marshalling support.
+
+ # See #as_json.
def self.json_create(object)
BigDecimal._load object['b']
end
- # Marshal the object to JSON.
+ # Methods <tt>BigDecimal#as_json</tt> and +BigDecimal.json_create+ may be used
+ # to serialize and deserialize a \BigDecimal object;
+ # see Marshal[rdoc-ref:Marshal].
+ #
+ # \Method <tt>BigDecimal#as_json</tt> serializes +self+,
+ # returning a 2-element hash representing +self+:
+ #
+ # require 'json/add/bigdecimal'
+ # x = BigDecimal(2).as_json # => {"json_class"=>"BigDecimal", "b"=>"27:0.2e1"}
+ # y = BigDecimal(2.0, 4).as_json # => {"json_class"=>"BigDecimal", "b"=>"36:0.2e1"}
+ # z = BigDecimal(Complex(2, 0)).as_json # => {"json_class"=>"BigDecimal", "b"=>"27:0.2e1"}
+ #
+ # \Method +JSON.create+ deserializes such a hash, returning a \BigDecimal object:
+ #
+ # BigDecimal.json_create(x) # => 0.2e1
+ # BigDecimal.json_create(y) # => 0.2e1
+ # BigDecimal.json_create(z) # => 0.2e1
#
- # method used for JSON marshalling support.
def as_json(*)
{
JSON.create_id => self.class.name,
- 'b' => _dump,
+ 'b' => _dump.force_encoding(Encoding::UTF_8),
}
end
- # return the JSON value
+ # Returns a JSON string representing +self+:
+ #
+ # require 'json/add/bigdecimal'
+ # puts BigDecimal(2).to_json
+ # puts BigDecimal(2.0, 4).to_json
+ # puts BigDecimal(Complex(2, 0)).to_json
+ #
+ # Output:
+ #
+ # {"json_class":"BigDecimal","b":"27:0.2e1"}
+ # {"json_class":"BigDecimal","b":"36:0.2e1"}
+ # {"json_class":"BigDecimal","b":"27:0.2e1"}
+ #
def to_json(*args)
as_json.to_json(*args)
end
-end
+end if defined?(::BigDecimal)
diff --git a/ext/json/lib/json/add/complex.rb b/ext/json/lib/json/add/complex.rb
index 4d977e7589..9e3c6f2d0a 100644
--- a/ext/json/lib/json/add/complex.rb
+++ b/ext/json/lib/json/add/complex.rb
@@ -1,19 +1,31 @@
-#frozen_string_literal: false
+# frozen_string_literal: true
unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
require 'json'
end
-defined?(::Complex) or require 'complex'
class Complex
- # Deserializes JSON string by converting Real value <tt>r</tt>, imaginary
- # value <tt>i</tt>, to a Complex object.
+ # See #as_json.
def self.json_create(object)
Complex(object['r'], object['i'])
end
- # Returns a hash, that will be turned into a JSON object and represent this
- # object.
+ # Methods <tt>Complex#as_json</tt> and +Complex.json_create+ may be used
+ # to serialize and deserialize a \Complex object;
+ # see Marshal[rdoc-ref:Marshal].
+ #
+ # \Method <tt>Complex#as_json</tt> serializes +self+,
+ # returning a 2-element hash representing +self+:
+ #
+ # require 'json/add/complex'
+ # x = Complex(2).as_json # => {"json_class"=>"Complex", "r"=>2, "i"=>0}
+ # y = Complex(2.0, 4).as_json # => {"json_class"=>"Complex", "r"=>2.0, "i"=>4}
+ #
+ # \Method +JSON.create+ deserializes such a hash, returning a \Complex object:
+ #
+ # Complex.json_create(x) # => (2+0i)
+ # Complex.json_create(y) # => (2.0+4i)
+ #
def as_json(*)
{
JSON.create_id => self.class.name,
@@ -22,7 +34,17 @@ class Complex
}
end
- # Stores class name (Complex) along with real value <tt>r</tt> and imaginary value <tt>i</tt> as JSON string
+ # Returns a JSON string representing +self+:
+ #
+ # require 'json/add/complex'
+ # puts Complex(2).to_json
+ # puts Complex(2.0, 4).to_json
+ #
+ # Output:
+ #
+ # {"json_class":"Complex","r":2,"i":0}
+ # {"json_class":"Complex","r":2.0,"i":4}
+ #
def to_json(*args)
as_json.to_json(*args)
end
diff --git a/ext/json/lib/json/add/core.rb b/ext/json/lib/json/add/core.rb
index bfb017c460..61ff454212 100644
--- a/ext/json/lib/json/add/core.rb
+++ b/ext/json/lib/json/add/core.rb
@@ -1,4 +1,4 @@
-#frozen_string_literal: false
+# frozen_string_literal: true
# This file requires the implementations of ruby core's custom objects for
# serialisation/deserialisation.
@@ -7,6 +7,7 @@ require 'json/add/date_time'
require 'json/add/exception'
require 'json/add/range'
require 'json/add/regexp'
+require 'json/add/string'
require 'json/add/struct'
require 'json/add/symbol'
require 'json/add/time'
diff --git a/ext/json/lib/json/add/date.rb b/ext/json/lib/json/add/date.rb
index 25523561a5..88a098b637 100644
--- a/ext/json/lib/json/add/date.rb
+++ b/ext/json/lib/json/add/date.rb
@@ -1,4 +1,4 @@
-#frozen_string_literal: false
+# frozen_string_literal: true
unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
require 'json'
end
@@ -6,16 +6,29 @@ require 'date'
class Date
- # Deserializes JSON string by converting Julian year <tt>y</tt>, month
- # <tt>m</tt>, day <tt>d</tt> and Day of Calendar Reform <tt>sg</tt> to Date.
+ # See #as_json.
def self.json_create(object)
civil(*object.values_at('y', 'm', 'd', 'sg'))
end
alias start sg unless method_defined?(:start)
- # Returns a hash, that will be turned into a JSON object and represent this
- # object.
+ # Methods <tt>Date#as_json</tt> and +Date.json_create+ may be used
+ # to serialize and deserialize a \Date object;
+ # see Marshal[rdoc-ref:Marshal].
+ #
+ # \Method <tt>Date#as_json</tt> serializes +self+,
+ # returning a 2-element hash representing +self+:
+ #
+ # require 'json/add/date'
+ # x = Date.today.as_json
+ # # => {"json_class"=>"Date", "y"=>2023, "m"=>11, "d"=>21, "sg"=>2299161.0}
+ #
+ # \Method +JSON.create+ deserializes such a hash, returning a \Date object:
+ #
+ # Date.json_create(x)
+ # # => #<Date: 2023-11-21 ((2460270j,0s,0n),+0s,2299161j)>
+ #
def as_json(*)
{
JSON.create_id => self.class.name,
@@ -26,8 +39,15 @@ class Date
}
end
- # Stores class name (Date) with Julian year <tt>y</tt>, month <tt>m</tt>, day
- # <tt>d</tt> and Day of Calendar Reform <tt>sg</tt> as JSON string
+ # Returns a JSON string representing +self+:
+ #
+ # require 'json/add/date'
+ # puts Date.today.to_json
+ #
+ # Output:
+ #
+ # {"json_class":"Date","y":2023,"m":11,"d":21,"sg":2299161.0}
+ #
def to_json(*args)
as_json.to_json(*args)
end
diff --git a/ext/json/lib/json/add/date_time.rb b/ext/json/lib/json/add/date_time.rb
index 38b0e86ab8..8b0bb5d181 100644
--- a/ext/json/lib/json/add/date_time.rb
+++ b/ext/json/lib/json/add/date_time.rb
@@ -1,4 +1,4 @@
-#frozen_string_literal: false
+# frozen_string_literal: true
unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
require 'json'
end
@@ -6,9 +6,7 @@ require 'date'
class DateTime
- # Deserializes JSON string by converting year <tt>y</tt>, month <tt>m</tt>,
- # day <tt>d</tt>, hour <tt>H</tt>, minute <tt>M</tt>, second <tt>S</tt>,
- # offset <tt>of</tt> and Day of Calendar Reform <tt>sg</tt> to DateTime.
+ # See #as_json.
def self.json_create(object)
args = object.values_at('y', 'm', 'd', 'H', 'M', 'S')
of_a, of_b = object['of'].split('/')
@@ -23,8 +21,21 @@ class DateTime
alias start sg unless method_defined?(:start)
- # Returns a hash, that will be turned into a JSON object and represent this
- # object.
+ # Methods <tt>DateTime#as_json</tt> and +DateTime.json_create+ may be used
+ # to serialize and deserialize a \DateTime object;
+ # see Marshal[rdoc-ref:Marshal].
+ #
+ # \Method <tt>DateTime#as_json</tt> serializes +self+,
+ # returning a 2-element hash representing +self+:
+ #
+ # require 'json/add/datetime'
+ # x = DateTime.now.as_json
+ # # => {"json_class"=>"DateTime", "y"=>2023, "m"=>11, "d"=>21, "sg"=>2299161.0}
+ #
+ # \Method +JSON.create+ deserializes such a hash, returning a \DateTime object:
+ #
+ # DateTime.json_create(x) # BUG? Raises Date::Error "invalid date"
+ #
def as_json(*)
{
JSON.create_id => self.class.name,
@@ -39,9 +50,15 @@ class DateTime
}
end
- # Stores class name (DateTime) with Julian year <tt>y</tt>, month <tt>m</tt>,
- # day <tt>d</tt>, hour <tt>H</tt>, minute <tt>M</tt>, second <tt>S</tt>,
- # offset <tt>of</tt> and Day of Calendar Reform <tt>sg</tt> as JSON string
+ # Returns a JSON string representing +self+:
+ #
+ # require 'json/add/datetime'
+ # puts DateTime.now.to_json
+ #
+ # Output:
+ #
+ # {"json_class":"DateTime","y":2023,"m":11,"d":21,"sg":2299161.0}
+ #
def to_json(*args)
as_json.to_json(*args)
end
diff --git a/ext/json/lib/json/add/exception.rb b/ext/json/lib/json/add/exception.rb
index a107e5b3c4..e85d404982 100644
--- a/ext/json/lib/json/add/exception.rb
+++ b/ext/json/lib/json/add/exception.rb
@@ -1,20 +1,31 @@
-#frozen_string_literal: false
+# frozen_string_literal: true
unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
require 'json'
end
class Exception
- # Deserializes JSON string by constructing new Exception object with message
- # <tt>m</tt> and backtrace <tt>b</tt> serialized with <tt>to_json</tt>
+ # See #as_json.
def self.json_create(object)
result = new(object['m'])
result.set_backtrace object['b']
result
end
- # Returns a hash, that will be turned into a JSON object and represent this
- # object.
+ # Methods <tt>Exception#as_json</tt> and +Exception.json_create+ may be used
+ # to serialize and deserialize a \Exception object;
+ # see Marshal[rdoc-ref:Marshal].
+ #
+ # \Method <tt>Exception#as_json</tt> serializes +self+,
+ # returning a 2-element hash representing +self+:
+ #
+ # require 'json/add/exception'
+ # x = Exception.new('Foo').as_json # => {"json_class"=>"Exception", "m"=>"Foo", "b"=>nil}
+ #
+ # \Method +JSON.create+ deserializes such a hash, returning a \Exception object:
+ #
+ # Exception.json_create(x) # => #<Exception: Foo>
+ #
def as_json(*)
{
JSON.create_id => self.class.name,
@@ -23,8 +34,15 @@ class Exception
}
end
- # Stores class name (Exception) with message <tt>m</tt> and backtrace array
- # <tt>b</tt> as JSON string
+ # Returns a JSON string representing +self+:
+ #
+ # require 'json/add/exception'
+ # puts Exception.new('Foo').to_json
+ #
+ # Output:
+ #
+ # {"json_class":"Exception","m":"Foo","b":null}
+ #
def to_json(*args)
as_json.to_json(*args)
end
diff --git a/ext/json/lib/json/add/ostruct.rb b/ext/json/lib/json/add/ostruct.rb
index 686cf0025d..7750498144 100644
--- a/ext/json/lib/json/add/ostruct.rb
+++ b/ext/json/lib/json/add/ostruct.rb
@@ -1,19 +1,35 @@
-#frozen_string_literal: false
+# frozen_string_literal: true
unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
require 'json'
end
-require 'ostruct'
+begin
+ require 'ostruct'
+rescue LoadError
+end
class OpenStruct
- # Deserializes JSON string by constructing new Struct object with values
- # <tt>t</tt> serialized by <tt>to_json</tt>.
+ # See #as_json.
def self.json_create(object)
new(object['t'] || object[:t])
end
- # Returns a hash, that will be turned into a JSON object and represent this
- # object.
+ # Methods <tt>OpenStruct#as_json</tt> and +OpenStruct.json_create+ may be used
+ # to serialize and deserialize a \OpenStruct object;
+ # see Marshal[rdoc-ref:Marshal].
+ #
+ # \Method <tt>OpenStruct#as_json</tt> serializes +self+,
+ # returning a 2-element hash representing +self+:
+ #
+ # require 'json/add/ostruct'
+ # x = OpenStruct.new('name' => 'Rowdy', :age => nil).as_json
+ # # => {"json_class"=>"OpenStruct", "t"=>{:name=>'Rowdy', :age=>nil}}
+ #
+ # \Method +JSON.create+ deserializes such a hash, returning a \OpenStruct object:
+ #
+ # OpenStruct.json_create(x)
+ # # => #<OpenStruct name='Rowdy', age=nil>
+ #
def as_json(*)
klass = self.class.name
klass.to_s.empty? and raise JSON::JSONError, "Only named structs are supported!"
@@ -23,9 +39,16 @@ class OpenStruct
}
end
- # Stores class name (OpenStruct) with this struct's values <tt>t</tt> as a
- # JSON string.
+ # Returns a JSON string representing +self+:
+ #
+ # require 'json/add/ostruct'
+ # puts OpenStruct.new('name' => 'Rowdy', :age => nil).to_json
+ #
+ # Output:
+ #
+ # {"json_class":"OpenStruct","t":{'name':'Rowdy',"age":null}}
+ #
def to_json(*args)
as_json.to_json(*args)
end
-end
+end if defined?(::OpenStruct)
diff --git a/ext/json/lib/json/add/range.rb b/ext/json/lib/json/add/range.rb
index 93529fb1c4..408d2c32f6 100644
--- a/ext/json/lib/json/add/range.rb
+++ b/ext/json/lib/json/add/range.rb
@@ -1,18 +1,33 @@
-#frozen_string_literal: false
+# frozen_string_literal: true
unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
require 'json'
end
class Range
- # Deserializes JSON string by constructing new Range object with arguments
- # <tt>a</tt> serialized by <tt>to_json</tt>.
+ # See #as_json.
def self.json_create(object)
new(*object['a'])
end
- # Returns a hash, that will be turned into a JSON object and represent this
- # object.
+ # Methods <tt>Range#as_json</tt> and +Range.json_create+ may be used
+ # to serialize and deserialize a \Range object;
+ # see Marshal[rdoc-ref:Marshal].
+ #
+ # \Method <tt>Range#as_json</tt> serializes +self+,
+ # returning a 2-element hash representing +self+:
+ #
+ # require 'json/add/range'
+ # x = (1..4).as_json # => {"json_class"=>"Range", "a"=>[1, 4, false]}
+ # y = (1...4).as_json # => {"json_class"=>"Range", "a"=>[1, 4, true]}
+ # z = ('a'..'d').as_json # => {"json_class"=>"Range", "a"=>["a", "d", false]}
+ #
+ # \Method +JSON.create+ deserializes such a hash, returning a \Range object:
+ #
+ # Range.json_create(x) # => 1..4
+ # Range.json_create(y) # => 1...4
+ # Range.json_create(z) # => "a".."d"
+ #
def as_json(*)
{
JSON.create_id => self.class.name,
@@ -20,9 +35,19 @@ class Range
}
end
- # Stores class name (Range) with JSON array of arguments <tt>a</tt> which
- # include <tt>first</tt> (integer), <tt>last</tt> (integer), and
- # <tt>exclude_end?</tt> (boolean) as JSON string.
+ # Returns a JSON string representing +self+:
+ #
+ # require 'json/add/range'
+ # puts (1..4).to_json
+ # puts (1...4).to_json
+ # puts ('a'..'d').to_json
+ #
+ # Output:
+ #
+ # {"json_class":"Range","a":[1,4,false]}
+ # {"json_class":"Range","a":[1,4,true]}
+ # {"json_class":"Range","a":["a","d",false]}
+ #
def to_json(*args)
as_json.to_json(*args)
end
diff --git a/ext/json/lib/json/add/rational.rb b/ext/json/lib/json/add/rational.rb
index 6be4034581..c95812ea8e 100644
--- a/ext/json/lib/json/add/rational.rb
+++ b/ext/json/lib/json/add/rational.rb
@@ -1,18 +1,31 @@
-#frozen_string_literal: false
+# frozen_string_literal: true
unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
require 'json'
end
-defined?(::Rational) or require 'rational'
class Rational
- # Deserializes JSON string by converting numerator value <tt>n</tt>,
- # denominator value <tt>d</tt>, to a Rational object.
+
+ # See #as_json.
def self.json_create(object)
Rational(object['n'], object['d'])
end
- # Returns a hash, that will be turned into a JSON object and represent this
- # object.
+ # Methods <tt>Rational#as_json</tt> and +Rational.json_create+ may be used
+ # to serialize and deserialize a \Rational object;
+ # see Marshal[rdoc-ref:Marshal].
+ #
+ # \Method <tt>Rational#as_json</tt> serializes +self+,
+ # returning a 2-element hash representing +self+:
+ #
+ # require 'json/add/rational'
+ # x = Rational(2, 3).as_json
+ # # => {"json_class"=>"Rational", "n"=>2, "d"=>3}
+ #
+ # \Method +JSON.create+ deserializes such a hash, returning a \Rational object:
+ #
+ # Rational.json_create(x)
+ # # => (2/3)
+ #
def as_json(*)
{
JSON.create_id => self.class.name,
@@ -21,7 +34,15 @@ class Rational
}
end
- # Stores class name (Rational) along with numerator value <tt>n</tt> and denominator value <tt>d</tt> as JSON string
+ # Returns a JSON string representing +self+:
+ #
+ # require 'json/add/rational'
+ # puts Rational(2, 3).to_json
+ #
+ # Output:
+ #
+ # {"json_class":"Rational","n":2,"d":3}
+ #
def to_json(*args)
as_json.to_json(*args)
end
diff --git a/ext/json/lib/json/add/regexp.rb b/ext/json/lib/json/add/regexp.rb
index 39d69fede7..aebfb2db5c 100644
--- a/ext/json/lib/json/add/regexp.rb
+++ b/ext/json/lib/json/add/regexp.rb
@@ -1,19 +1,30 @@
-#frozen_string_literal: false
+# frozen_string_literal: true
unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
require 'json'
end
class Regexp
- # Deserializes JSON string by constructing new Regexp object with source
- # <tt>s</tt> (Regexp or String) and options <tt>o</tt> serialized by
- # <tt>to_json</tt>
+ # See #as_json.
def self.json_create(object)
new(object['s'], object['o'])
end
- # Returns a hash, that will be turned into a JSON object and represent this
- # object.
+ # Methods <tt>Regexp#as_json</tt> and +Regexp.json_create+ may be used
+ # to serialize and deserialize a \Regexp object;
+ # see Marshal[rdoc-ref:Marshal].
+ #
+ # \Method <tt>Regexp#as_json</tt> serializes +self+,
+ # returning a 2-element hash representing +self+:
+ #
+ # require 'json/add/regexp'
+ # x = /foo/.as_json
+ # # => {"json_class"=>"Regexp", "o"=>0, "s"=>"foo"}
+ #
+ # \Method +JSON.create+ deserializes such a hash, returning a \Regexp object:
+ #
+ # Regexp.json_create(x) # => /foo/
+ #
def as_json(*)
{
JSON.create_id => self.class.name,
@@ -22,8 +33,15 @@ class Regexp
}
end
- # Stores class name (Regexp) with options <tt>o</tt> and source <tt>s</tt>
- # (Regexp or String) as JSON string
+ # Returns a JSON string representing +self+:
+ #
+ # require 'json/add/regexp'
+ # puts /foo/.to_json
+ #
+ # Output:
+ #
+ # {"json_class":"Regexp","o":0,"s":"foo"}
+ #
def to_json(*args)
as_json.to_json(*args)
end
diff --git a/ext/json/lib/json/add/set.rb b/ext/json/lib/json/add/set.rb
index 71e2a0ac8b..1918353187 100644
--- a/ext/json/lib/json/add/set.rb
+++ b/ext/json/lib/json/add/set.rb
@@ -4,16 +4,27 @@ end
defined?(::Set) or require 'set'
class Set
- # Import a JSON Marshalled object.
- #
- # method used for JSON marshalling support.
+
+ # See #as_json.
def self.json_create(object)
new object['a']
end
- # Marshal the object to JSON.
+ # Methods <tt>Set#as_json</tt> and +Set.json_create+ may be used
+ # to serialize and deserialize a \Set object;
+ # see Marshal[rdoc-ref:Marshal].
+ #
+ # \Method <tt>Set#as_json</tt> serializes +self+,
+ # returning a 2-element hash representing +self+:
+ #
+ # require 'json/add/set'
+ # x = Set.new(%w/foo bar baz/).as_json
+ # # => {"json_class"=>"Set", "a"=>["foo", "bar", "baz"]}
+ #
+ # \Method +JSON.create+ deserializes such a hash, returning a \Set object:
+ #
+ # Set.json_create(x) # => #<Set: {"foo", "bar", "baz"}>
#
- # method used for JSON marshalling support.
def as_json(*)
{
JSON.create_id => self.class.name,
@@ -21,7 +32,15 @@ class Set
}
end
- # return the JSON value
+ # Returns a JSON string representing +self+:
+ #
+ # require 'json/add/set'
+ # puts Set.new(%w/foo bar baz/).to_json
+ #
+ # Output:
+ #
+ # {"json_class":"Set","a":["foo","bar","baz"]}
+ #
def to_json(*args)
as_json.to_json(*args)
end
diff --git a/ext/json/lib/json/add/string.rb b/ext/json/lib/json/add/string.rb
new file mode 100644
index 0000000000..9c3bde27fb
--- /dev/null
+++ b/ext/json/lib/json/add/string.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
+ require 'json'
+end
+
+class String
+ # call-seq: json_create(o)
+ #
+ # Raw Strings are JSON Objects (the raw bytes are stored in an array for the
+ # key "raw"). The Ruby String can be created by this class method.
+ def self.json_create(object)
+ object["raw"].pack("C*")
+ end
+
+ # call-seq: to_json_raw_object()
+ #
+ # This method creates a raw object hash, that can be nested into
+ # other data structures and will be generated as a raw string. This
+ # method should be used, if you want to convert raw strings to JSON
+ # instead of UTF-8 strings, e. g. binary data.
+ def to_json_raw_object
+ {
+ JSON.create_id => self.class.name,
+ "raw" => unpack("C*"),
+ }
+ end
+
+ # call-seq: to_json_raw(*args)
+ #
+ # This method creates a JSON text from the result of a call to
+ # to_json_raw_object of this String.
+ def to_json_raw(...)
+ to_json_raw_object.to_json(...)
+ end
+end
diff --git a/ext/json/lib/json/add/struct.rb b/ext/json/lib/json/add/struct.rb
index e8395ed42f..6760c3d86c 100644
--- a/ext/json/lib/json/add/struct.rb
+++ b/ext/json/lib/json/add/struct.rb
@@ -1,18 +1,32 @@
-#frozen_string_literal: false
+# frozen_string_literal: true
unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
require 'json'
end
class Struct
- # Deserializes JSON string by constructing new Struct object with values
- # <tt>v</tt> serialized by <tt>to_json</tt>.
+ # See #as_json.
def self.json_create(object)
new(*object['v'])
end
- # Returns a hash, that will be turned into a JSON object and represent this
- # object.
+ # Methods <tt>Struct#as_json</tt> and +Struct.json_create+ may be used
+ # to serialize and deserialize a \Struct object;
+ # see Marshal[rdoc-ref:Marshal].
+ #
+ # \Method <tt>Struct#as_json</tt> serializes +self+,
+ # returning a 2-element hash representing +self+:
+ #
+ # require 'json/add/struct'
+ # Customer = Struct.new('Customer', :name, :address, :zip)
+ # x = Struct::Customer.new.as_json
+ # # => {"json_class"=>"Struct::Customer", "v"=>[nil, nil, nil]}
+ #
+ # \Method +JSON.create+ deserializes such a hash, returning a \Struct object:
+ #
+ # Struct::Customer.json_create(x)
+ # # => #<struct Struct::Customer name=nil, address=nil, zip=nil>
+ #
def as_json(*)
klass = self.class.name
klass.to_s.empty? and raise JSON::JSONError, "Only named structs are supported!"
@@ -22,8 +36,16 @@ class Struct
}
end
- # Stores class name (Struct) with Struct values <tt>v</tt> as a JSON string.
- # Only named structs are supported.
+ # Returns a JSON string representing +self+:
+ #
+ # require 'json/add/struct'
+ # Customer = Struct.new('Customer', :name, :address, :zip)
+ # puts Struct::Customer.new.to_json
+ #
+ # Output:
+ #
+ # {"json_class":"Struct","t":{'name':'Rowdy',"age":null}}
+ #
def to_json(*args)
as_json.to_json(*args)
end
diff --git a/ext/json/lib/json/add/symbol.rb b/ext/json/lib/json/add/symbol.rb
index 74b13a423f..806be4f025 100644
--- a/ext/json/lib/json/add/symbol.rb
+++ b/ext/json/lib/json/add/symbol.rb
@@ -1,11 +1,25 @@
-#frozen_string_literal: false
+# frozen_string_literal: true
unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
require 'json'
end
class Symbol
- # Returns a hash, that will be turned into a JSON object and represent this
- # object.
+
+ # Methods <tt>Symbol#as_json</tt> and +Symbol.json_create+ may be used
+ # to serialize and deserialize a \Symbol object;
+ # see Marshal[rdoc-ref:Marshal].
+ #
+ # \Method <tt>Symbol#as_json</tt> serializes +self+,
+ # returning a 2-element hash representing +self+:
+ #
+ # require 'json/add/symbol'
+ # x = :foo.as_json
+ # # => {"json_class"=>"Symbol", "s"=>"foo"}
+ #
+ # \Method +JSON.create+ deserializes such a hash, returning a \Symbol object:
+ #
+ # Symbol.json_create(x) # => :foo
+ #
def as_json(*)
{
JSON.create_id => self.class.name,
@@ -13,12 +27,25 @@ class Symbol
}
end
- # Stores class name (Symbol) with String representation of Symbol as a JSON string.
- def to_json(*a)
- as_json.to_json(*a)
+ # Returns a JSON string representing +self+:
+ #
+ # require 'json/add/symbol'
+ # puts :foo.to_json
+ #
+ # Output:
+ #
+ # # {"json_class":"Symbol","s":"foo"}
+ #
+ def to_json(state = nil, *a)
+ state = ::JSON::State.from_state(state)
+ if state.strict?
+ super
+ else
+ as_json.to_json(state, *a)
+ end
end
- # Deserializes JSON string by converting the <tt>string</tt> value stored in the object to a Symbol
+ # See #as_json.
def self.json_create(o)
o['s'].to_sym
end
diff --git a/ext/json/lib/json/add/time.rb b/ext/json/lib/json/add/time.rb
index b73acc4086..b03d4ff251 100644
--- a/ext/json/lib/json/add/time.rb
+++ b/ext/json/lib/json/add/time.rb
@@ -1,37 +1,51 @@
-#frozen_string_literal: false
+# frozen_string_literal: true
unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
require 'json'
end
class Time
- # Deserializes JSON string by converting time since epoch to Time
+ # See #as_json.
def self.json_create(object)
if usec = object.delete('u') # used to be tv_usec -> tv_nsec
object['n'] = usec * 1000
end
- if method_defined?(:tv_nsec)
- at(object['s'], Rational(object['n'], 1000))
- else
- at(object['s'], object['n'] / 1000)
- end
+ at(object['s'], Rational(object['n'], 1000))
end
- # Returns a hash, that will be turned into a JSON object and represent this
- # object.
+ # Methods <tt>Time#as_json</tt> and +Time.json_create+ may be used
+ # to serialize and deserialize a \Time object;
+ # see Marshal[rdoc-ref:Marshal].
+ #
+ # \Method <tt>Time#as_json</tt> serializes +self+,
+ # returning a 2-element hash representing +self+:
+ #
+ # require 'json/add/time'
+ # x = Time.now.as_json
+ # # => {"json_class"=>"Time", "s"=>1700931656, "n"=>472846644}
+ #
+ # \Method +JSON.create+ deserializes such a hash, returning a \Time object:
+ #
+ # Time.json_create(x)
+ # # => 2023-11-25 11:00:56.472846644 -0600
+ #
def as_json(*)
- nanoseconds = [ tv_usec * 1000 ]
- respond_to?(:tv_nsec) and nanoseconds << tv_nsec
- nanoseconds = nanoseconds.max
{
JSON.create_id => self.class.name,
's' => tv_sec,
- 'n' => nanoseconds,
+ 'n' => tv_nsec,
}
end
- # Stores class name (Time) with number of seconds since epoch and number of
- # microseconds for Time as JSON string
+ # Returns a JSON string representing +self+:
+ #
+ # require 'json/add/time'
+ # puts Time.now.to_json
+ #
+ # Output:
+ #
+ # {"json_class":"Time","s":1700931678,"n":980650786}
+ #
def to_json(*args)
as_json.to_json(*args)
end
diff --git a/ext/json/lib/json/common.rb b/ext/json/lib/json/common.rb
index 3be9fd8dc5..230bf08012 100644
--- a/ext/json/lib/json/common.rb
+++ b/ext/json/lib/json/common.rb
@@ -1,25 +1,148 @@
-#frozen_string_literal: false
+# frozen_string_literal: true
+
require 'json/version'
-require 'json/generic_object'
module JSON
+ autoload :GenericObject, 'json/generic_object'
+
+ module ParserOptions # :nodoc:
+ class << self
+ def prepare(opts)
+ if opts[:object_class] || opts[:array_class]
+ opts = opts.dup
+ on_load = opts[:on_load]
+
+ on_load = object_class_proc(opts[:object_class], on_load) if opts[:object_class]
+ on_load = array_class_proc(opts[:array_class], on_load) if opts[:array_class]
+ opts[:on_load] = on_load
+ end
+
+ if opts.fetch(:create_additions, false) != false
+ opts = create_additions_proc(opts)
+ end
+
+ opts
+ end
+
+ private
+
+ def object_class_proc(object_class, on_load)
+ ->(obj) do
+ if Hash === obj
+ object = object_class.new
+ obj.each { |k, v| object[k] = v }
+ obj = object
+ end
+ on_load.nil? ? obj : on_load.call(obj)
+ end
+ end
+
+ def array_class_proc(array_class, on_load)
+ ->(obj) do
+ if Array === obj
+ array = array_class.new
+ obj.each { |v| array << v }
+ obj = array
+ end
+ on_load.nil? ? obj : on_load.call(obj)
+ end
+ end
+
+ # TODO: extract :create_additions support to another gem for version 3.0
+ def create_additions_proc(opts)
+ if opts[:symbolize_names]
+ raise ArgumentError, "options :symbolize_names and :create_additions cannot be used in conjunction"
+ end
+
+ opts = opts.dup
+ create_additions = opts.fetch(:create_additions, false)
+ on_load = opts[:on_load]
+ object_class = opts[:object_class] || Hash
+
+ opts[:on_load] = ->(object) do
+ case object
+ when String
+ opts[:match_string]&.each do |pattern, klass|
+ if match = pattern.match(object)
+ create_additions_warning if create_additions.nil?
+ object = klass.json_create(object)
+ break
+ end
+ end
+ when object_class
+ if opts[:create_additions] != false
+ if class_path = object[JSON.create_id]
+ klass = begin
+ Object.const_get(class_path)
+ rescue NameError => e
+ raise ArgumentError, "can't get const #{class_path}: #{e}"
+ end
+
+ if klass.respond_to?(:json_creatable?) ? klass.json_creatable? : klass.respond_to?(:json_create)
+ create_additions_warning if create_additions.nil?
+ object = klass.json_create(object)
+ end
+ end
+ end
+ end
+
+ on_load.nil? ? object : on_load.call(object)
+ end
+
+ opts
+ end
+
+ def create_additions_warning
+ JSON.deprecation_warning "JSON.load implicit support for `create_additions: true` is deprecated " \
+ "and will be removed in 3.0, use JSON.unsafe_load or explicitly " \
+ "pass `create_additions: true`"
+ end
+ end
+ end
+
class << self
- # If _object_ is string-like, parse the string and return the parsed
- # result as a Ruby data structure. Otherwise generate a JSON text from the
- # Ruby data structure object and return it.
- #
- # The _opts_ argument is passed through to generate/parse respectively.
- # See generate and parse for their documentation.
- def [](object, opts = {})
- if object.respond_to? :to_str
- JSON.parse(object.to_str, opts)
+ def deprecation_warning(message, uplevel = 3) # :nodoc:
+ gem_root = File.expand_path("..", __dir__) + "/"
+ caller_locations(uplevel, 10).each do |frame|
+ if frame.path.nil? || frame.path.start_with?(gem_root) || frame.path.end_with?("/truffle/cext_ruby.rb", ".c")
+ uplevel += 1
+ else
+ break
+ end
+ end
+
+ if RUBY_VERSION >= "3.0"
+ warn(message, uplevel: uplevel, category: :deprecated)
else
- JSON.generate(object, opts)
+ warn(message, uplevel: uplevel)
end
end
- # Returns the JSON parser class that is used by JSON. This is either
- # JSON::Ext::Parser or JSON::Pure::Parser.
+ # :call-seq:
+ # JSON[object] -> new_array or new_string
+ #
+ # If +object+ is a \String,
+ # calls JSON.parse with +object+ and +opts+ (see method #parse):
+ # json = '[0, 1, null]'
+ # JSON[json]# => [0, 1, nil]
+ #
+ # Otherwise, calls JSON.generate with +object+ and +opts+ (see method #generate):
+ # ruby = [0, 1, nil]
+ # JSON[ruby] # => '[0,1,null]'
+ def [](object, opts = nil)
+ if object.is_a?(String)
+ return JSON.parse(object, opts)
+ elsif object.respond_to?(:to_str)
+ str = object.to_str
+ if str.is_a?(String)
+ return JSON.parse(str, opts)
+ end
+ end
+
+ JSON.generate(object, opts)
+ end
+
+ # Returns the JSON parser class that is used by JSON.
attr_reader :parser
# Set the JSON parser class _parser_ to be used by JSON.
@@ -29,382 +152,975 @@ module JSON
const_set :Parser, parser
end
- # Return the constant located at _path_. The format of _path_ has to be
- # either ::A::B::C or A::B::C. In any case, A has to be located at the top
- # level (absolute namespace path?). If there doesn't exist a constant at
- # the given path, an ArgumentError is raised.
- def deep_const_get(path) # :nodoc:
- path.to_s.split(/::/).inject(Object) do |p, c|
- case
- when c.empty? then p
- when p.const_defined?(c, true) then p.const_get(c)
- else
- begin
- p.const_missing(c)
- rescue NameError => e
- raise ArgumentError, "can't get const #{path}: #{e}"
- end
- end
- end
- end
-
# Set the module _generator_ to be used by JSON.
def generator=(generator) # :nodoc:
old, $VERBOSE = $VERBOSE, nil
@generator = generator
- generator_methods = generator::GeneratorMethods
- for const in generator_methods.constants
- klass = deep_const_get(const)
- modul = generator_methods.const_get(const)
- klass.class_eval do
- instance_methods(false).each do |m|
- m.to_s == 'to_json' and remove_method m
+ if generator.const_defined?(:GeneratorMethods)
+ generator_methods = generator::GeneratorMethods
+ for const in generator_methods.constants
+ klass = const_get(const)
+ modul = generator_methods.const_get(const)
+ klass.class_eval do
+ instance_methods(false).each do |m|
+ m.to_s == 'to_json' and remove_method m
+ end
+ include modul
end
- include modul
end
end
self.state = generator::State
- const_set :State, self.state
- const_set :SAFE_STATE_PROTOTYPE, State.new
- const_set :FAST_STATE_PROTOTYPE, State.new(
- :indent => '',
- :space => '',
- :object_nl => "",
- :array_nl => "",
- :max_nesting => false
- )
- const_set :PRETTY_STATE_PROTOTYPE, State.new(
- :indent => ' ',
- :space => ' ',
- :object_nl => "\n",
- :array_nl => "\n"
- )
+ const_set :State, state
ensure
$VERBOSE = old
end
- # Returns the JSON generator module that is used by JSON. This is
- # either JSON::Ext::Generator or JSON::Pure::Generator.
+ # Returns the JSON generator module that is used by JSON.
attr_reader :generator
- # Returns the JSON generator state class that is used by JSON. This is
- # either JSON::Ext::Generator::State or JSON::Pure::Generator::State.
+ # Sets or Returns the JSON generator state class that is used by JSON.
attr_accessor :state
- # This is create identifier, which is used to decide if the _json_create_
- # hook of a class should be called. It defaults to 'json_class'.
- attr_accessor :create_id
+ private
+
+ # Called from the extension when a hash has both string and symbol keys
+ def on_mixed_keys_hash(hash, do_raise)
+ set = {}
+ hash.each_key do |key|
+ key_str = key.to_s
+
+ if set[key_str]
+ message = "detected duplicate key #{key_str.inspect} in #{hash.inspect}"
+ if do_raise
+ raise GeneratorError, message
+ else
+ deprecation_warning("#{message}.\nThis will raise an error in json 3.0 unless enabled via `allow_duplicate_key: true`")
+ end
+ else
+ set[key_str] = true
+ end
+ end
+ end
+
+ def deprecated_singleton_attr_accessor(*attrs)
+ args = RUBY_VERSION >= "3.0" ? ", category: :deprecated" : ""
+ attrs.each do |attr|
+ singleton_class.class_eval <<~RUBY
+ def #{attr}
+ warn "JSON.#{attr} is deprecated and will be removed in json 3.0.0", uplevel: 1 #{args}
+ @#{attr}
+ end
+
+ def #{attr}=(val)
+ warn "JSON.#{attr}= is deprecated and will be removed in json 3.0.0", uplevel: 1 #{args}
+ @#{attr} = val
+ end
+
+ def _#{attr}
+ @#{attr}
+ end
+ RUBY
+ end
+ end
end
- self.create_id = 'json_class'
- NaN = 0.0/0
+ # Sets create identifier, which is used to decide if the _json_create_
+ # hook of a class should be called; initial value is +json_class+:
+ # JSON.create_id # => 'json_class'
+ def self.create_id=(new_value)
+ Thread.current[:"JSON.create_id"] = new_value.dup.freeze
+ end
+
+ # Returns the current create identifier.
+ # See also JSON.create_id=.
+ def self.create_id
+ Thread.current[:"JSON.create_id"] || 'json_class'
+ end
- Infinity = 1.0/0
+ NaN = Float::NAN
+
+ Infinity = Float::INFINITY
MinusInfinity = -Infinity
# The base exception for JSON errors.
- class JSONError < StandardError
- def self.wrap(exception)
- obj = new("Wrapped(#{exception.class}): #{exception.message.inspect}")
- obj.set_backtrace exception.backtrace
- obj
- end
- end
+ class JSONError < StandardError; end
# This exception is raised if a parser error occurs.
- class ParserError < JSONError; end
+ class ParserError < JSONError
+ attr_reader :line, :column
+ end
# This exception is raised if the nesting of parsed data structures is too
# deep.
class NestingError < ParserError; end
- # :stopdoc:
- class CircularDatastructure < NestingError; end
- # :startdoc:
-
# This exception is raised if a generator or unparser error occurs.
- class GeneratorError < JSONError; end
- # For backwards compatibility
- UnparserError = GeneratorError
+ class GeneratorError < JSONError
+ attr_reader :invalid_object
+
+ def initialize(message, invalid_object = nil)
+ super(message)
+ @invalid_object = invalid_object
+ end
- # This exception is raised if the required unicode support is missing on the
- # system. Usually this means that the iconv library is not installed.
- class MissingUnicodeSupport < JSONError; end
+ def detailed_message(...)
+ # Exception#detailed_message doesn't exist until Ruby 3.2
+ super_message = defined?(super) ? super : message
+
+ if @invalid_object.nil?
+ super_message
+ else
+ "#{super_message}\nInvalid object: #{@invalid_object.inspect}"
+ end
+ end
+ end
+
+ # Fragment of JSON document that is to be included as is:
+ # fragment = JSON::Fragment.new("[1, 2, 3]")
+ # JSON.generate({ count: 3, items: fragments })
+ #
+ # This allows to easily assemble multiple JSON fragments that have
+ # been persisted somewhere without having to parse them nor resorting
+ # to string interpolation.
+ #
+ # Note: no validation is performed on the provided string. It is the
+ # responsibility of the caller to ensure the string contains valid JSON.
+ Fragment = Struct.new(:json) do
+ def initialize(json)
+ unless string = String.try_convert(json)
+ raise TypeError, " no implicit conversion of #{json.class} into String"
+ end
+
+ super(string)
+ end
+
+ def to_json(state = nil, *)
+ json
+ end
+ end
module_function
- # Parse the JSON document _source_ into a Ruby data structure and return it.
- #
- # _opts_ can have the following
- # keys:
- # * *max_nesting*: The maximum depth of nesting allowed in the parsed data
- # structures. Disable depth checking with :max_nesting => false. It
- # defaults to 100.
- # * *allow_nan*: If set to true, allow NaN, Infinity and -Infinity in
- # defiance of RFC 7159 to be parsed by the Parser. This option defaults
- # to false.
- # * *symbolize_names*: If set to true, returns symbols for the names
- # (keys) in a JSON object. Otherwise strings are returned. Strings are
- # the default.
- # * *create_additions*: If set to false, the Parser doesn't create
- # additions even if a matching class and create_id was found. This option
- # defaults to false.
- # * *object_class*: Defaults to Hash
- # * *array_class*: Defaults to Array
- def parse(source, opts = {})
- Parser.new(source, **(opts||{})).parse
+ # :call-seq:
+ # JSON.parse(source, opts) -> object
+ #
+ # Returns the Ruby objects created by parsing the given +source+.
+ #
+ # Argument +source+ contains the \String to be parsed.
+ #
+ # Argument +opts+, if given, contains a \Hash of options for the parsing.
+ # See {Parsing Options}[#module-JSON-label-Parsing+Options].
+ #
+ # ---
+ #
+ # When +source+ is a \JSON array, returns a Ruby \Array:
+ # source = '["foo", 1.0, true, false, null]'
+ # ruby = JSON.parse(source)
+ # ruby # => ["foo", 1.0, true, false, nil]
+ # ruby.class # => Array
+ #
+ # When +source+ is a \JSON object, returns a Ruby \Hash:
+ # source = '{"a": "foo", "b": 1.0, "c": true, "d": false, "e": null}'
+ # ruby = JSON.parse(source)
+ # ruby # => {"a"=>"foo", "b"=>1.0, "c"=>true, "d"=>false, "e"=>nil}
+ # ruby.class # => Hash
+ #
+ # For examples of parsing for all \JSON data types, see
+ # {Parsing \JSON}[#module-JSON-label-Parsing+JSON].
+ #
+ # Parses nested JSON objects:
+ # source = <<~JSON
+ # {
+ # "name": "Dave",
+ # "age" :40,
+ # "hats": [
+ # "Cattleman's",
+ # "Panama",
+ # "Tophat"
+ # ]
+ # }
+ # JSON
+ # ruby = JSON.parse(source)
+ # ruby # => {"name"=>"Dave", "age"=>40, "hats"=>["Cattleman's", "Panama", "Tophat"]}
+ #
+ # ---
+ #
+ # Raises an exception if +source+ is not valid JSON:
+ # # Raises JSON::ParserError (783: unexpected token at ''):
+ # JSON.parse('')
+ #
+ def parse(source, opts = nil)
+ opts = ParserOptions.prepare(opts) unless opts.nil?
+ Parser.parse(source, opts)
+ end
+
+ PARSE_L_OPTIONS = {
+ max_nesting: false,
+ allow_nan: true,
+ }.freeze
+ private_constant :PARSE_L_OPTIONS
+
+ # :call-seq:
+ # JSON.parse!(source, opts) -> object
+ #
+ # Calls
+ # parse(source, opts)
+ # with +source+ and possibly modified +opts+.
+ #
+ # Differences from JSON.parse:
+ # - Option +max_nesting+, if not provided, defaults to +false+,
+ # which disables checking for nesting depth.
+ # - Option +allow_nan+, if not provided, defaults to +true+.
+ def parse!(source, opts = nil)
+ if opts.nil?
+ parse(source, PARSE_L_OPTIONS)
+ else
+ parse(source, PARSE_L_OPTIONS.merge(opts))
+ end
+ end
+
+ # :call-seq:
+ # JSON.load_file(path, opts={}) -> object
+ #
+ # Calls:
+ # parse(File.read(path), opts)
+ #
+ # See method #parse.
+ def load_file(filespec, opts = nil)
+ parse(File.read(filespec, encoding: Encoding::UTF_8), opts)
end
- # Parse the JSON document _source_ into a Ruby data structure and return it.
- # The bang version of the parse method defaults to the more dangerous values
- # for the _opts_ hash, so be sure only to parse trusted _source_ documents.
- #
- # _opts_ can have the following keys:
- # * *max_nesting*: The maximum depth of nesting allowed in the parsed data
- # structures. Enable depth checking with :max_nesting => anInteger. The
- # parse! methods defaults to not doing max depth checking: This can be
- # dangerous if someone wants to fill up your stack.
- # * *allow_nan*: If set to true, allow NaN, Infinity, and -Infinity in
- # defiance of RFC 7159 to be parsed by the Parser. This option defaults
- # to true.
- # * *create_additions*: If set to false, the Parser doesn't create
- # additions even if a matching class and create_id was found. This option
- # defaults to false.
- def parse!(source, opts = {})
- opts = {
- :max_nesting => false,
- :allow_nan => true
- }.merge(opts)
- Parser.new(source, **(opts||{})).parse
+ # :call-seq:
+ # JSON.load_file!(path, opts = {})
+ #
+ # Calls:
+ # JSON.parse!(File.read(path, opts))
+ #
+ # See method #parse!
+ def load_file!(filespec, opts = nil)
+ parse!(File.read(filespec, encoding: Encoding::UTF_8), opts)
end
- # Generate a JSON document from the Ruby data structure _obj_ and return
- # it. _state_ is * a JSON::State object,
- # * or a Hash like object (responding to to_hash),
- # * an object convertible into a hash by a to_h method,
- # that is used as or to configure a State object.
- #
- # It defaults to a state object, that creates the shortest possible JSON text
- # in one line, checks for circular data structures and doesn't allow NaN,
- # Infinity, and -Infinity.
- #
- # A _state_ hash can have the following keys:
- # * *indent*: a string used to indent levels (default: ''),
- # * *space*: a string that is put after, a : or , delimiter (default: ''),
- # * *space_before*: a string that is put before a : pair delimiter (default: ''),
- # * *object_nl*: a string that is put at the end of a JSON object (default: ''),
- # * *array_nl*: a string that is put at the end of a JSON array (default: ''),
- # * *allow_nan*: true if NaN, Infinity, and -Infinity should be
- # generated, otherwise an exception is thrown if these values are
- # encountered. This options defaults to false.
- # * *max_nesting*: The maximum depth of nesting allowed in the data
- # structures from which JSON is to be generated. Disable depth checking
- # with :max_nesting => false, it defaults to 100.
- #
- # See also the fast_generate for the fastest creation method with the least
- # amount of sanity checks, and the pretty_generate method for some
- # defaults for pretty output.
+ # :call-seq:
+ # JSON.generate(obj, opts = nil) -> new_string
+ #
+ # Returns a \String containing the generated \JSON data.
+ #
+ # See also JSON.pretty_generate.
+ #
+ # Argument +obj+ is the Ruby object to be converted to \JSON.
+ #
+ # Argument +opts+, if given, contains a \Hash of options for the generation.
+ # See {Generating Options}[#module-JSON-label-Generating+Options].
+ #
+ # ---
+ #
+ # When +obj+ is an \Array, returns a \String containing a \JSON array:
+ # obj = ["foo", 1.0, true, false, nil]
+ # json = JSON.generate(obj)
+ # json # => '["foo",1.0,true,false,null]'
+ #
+ # When +obj+ is a \Hash, returns a \String containing a \JSON object:
+ # obj = {foo: 0, bar: 's', baz: :bat}
+ # json = JSON.generate(obj)
+ # json # => '{"foo":0,"bar":"s","baz":"bat"}'
+ #
+ # For examples of generating from other Ruby objects, see
+ # {Generating \JSON from Other Objects}[#module-JSON-label-Generating+JSON+from+Other+Objects].
+ #
+ # ---
+ #
+ # Raises an exception if any formatting option is not a \String.
+ #
+ # Raises an exception if +obj+ contains circular references:
+ # a = []; b = []; a.push(b); b.push(a)
+ # # Raises JSON::NestingError (nesting of 100 is too deep):
+ # JSON.generate(a)
+ #
def generate(obj, opts = nil)
if State === opts
- state, opts = opts, nil
+ opts.generate(obj)
else
- state = SAFE_STATE_PROTOTYPE.dup
+ State.generate(obj, opts, nil)
end
- if opts
- if opts.respond_to? :to_hash
- opts = opts.to_hash
- elsif opts.respond_to? :to_h
- opts = opts.to_h
- else
- raise TypeError, "can't convert #{opts.class} into Hash"
- end
- state = state.configure(opts)
- end
- state.generate(obj)
end
- # :stopdoc:
- # I want to deprecate these later, so I'll first be silent about them, and
- # later delete them.
- alias unparse generate
- module_function :unparse
- # :startdoc:
-
- # Generate a JSON document from the Ruby data structure _obj_ and return it.
- # This method disables the checks for circles in Ruby objects.
+ # :call-seq:
+ # JSON.fast_generate(obj, opts) -> new_string
#
- # *WARNING*: Be careful not to pass any Ruby data structures with circles as
- # _obj_ argument because this will cause JSON to go into an infinite loop.
+ # Arguments +obj+ and +opts+ here are the same as
+ # arguments +obj+ and +opts+ in JSON.generate.
+ #
+ # By default, generates \JSON data without checking
+ # for circular references in +obj+ (option +max_nesting+ set to +false+, disabled).
+ #
+ # Raises an exception if +obj+ contains circular references:
+ # a = []; b = []; a.push(b); b.push(a)
+ # # Raises SystemStackError (stack level too deep):
+ # JSON.fast_generate(a)
def fast_generate(obj, opts = nil)
- if State === opts
- state, opts = opts, nil
+ if RUBY_VERSION >= "3.0"
+ warn "JSON.fast_generate is deprecated and will be removed in json 3.0.0, just use JSON.generate", uplevel: 1, category: :deprecated
else
- state = FAST_STATE_PROTOTYPE.dup
- end
- if opts
- if opts.respond_to? :to_hash
- opts = opts.to_hash
- elsif opts.respond_to? :to_h
- opts = opts.to_h
- else
- raise TypeError, "can't convert #{opts.class} into Hash"
- end
- state.configure(opts)
+ warn "JSON.fast_generate is deprecated and will be removed in json 3.0.0, just use JSON.generate", uplevel: 1
end
- state.generate(obj)
+ generate(obj, opts)
end
- # :stopdoc:
- # I want to deprecate these later, so I'll first be silent about them, and later delete them.
- alias fast_unparse fast_generate
- module_function :fast_unparse
- # :startdoc:
+ PRETTY_GENERATE_OPTIONS = {
+ indent: ' ',
+ space: ' ',
+ object_nl: "\n",
+ array_nl: "\n",
+ }.freeze
+ private_constant :PRETTY_GENERATE_OPTIONS
- # Generate a JSON document from the Ruby data structure _obj_ and return it.
- # The returned document is a prettier form of the document returned by
- # #unparse.
+ # :call-seq:
+ # JSON.pretty_generate(obj, opts = nil) -> new_string
+ #
+ # Arguments +obj+ and +opts+ here are the same as
+ # arguments +obj+ and +opts+ in JSON.generate.
+ #
+ # Default options are:
+ # {
+ # indent: ' ', # Two spaces
+ # space: ' ', # One space
+ # array_nl: "\n", # Newline
+ # object_nl: "\n" # Newline
+ # }
+ #
+ # Example:
+ # obj = {foo: [:bar, :baz], bat: {bam: 0, bad: 1}}
+ # json = JSON.pretty_generate(obj)
+ # puts json
+ # Output:
+ # {
+ # "foo": [
+ # "bar",
+ # "baz"
+ # ],
+ # "bat": {
+ # "bam": 0,
+ # "bad": 1
+ # }
+ # }
#
- # The _opts_ argument can be used to configure the generator. See the
- # generate method for a more detailed explanation.
def pretty_generate(obj, opts = nil)
- if State === opts
- state, opts = opts, nil
- else
- state = PRETTY_STATE_PROTOTYPE.dup
- end
+ return opts.generate(obj) if State === opts
+
+ options = PRETTY_GENERATE_OPTIONS
+
if opts
- if opts.respond_to? :to_hash
- opts = opts.to_hash
- elsif opts.respond_to? :to_h
- opts = opts.to_h
- else
- raise TypeError, "can't convert #{opts.class} into Hash"
+ unless opts.is_a?(Hash)
+ if opts.respond_to? :to_hash
+ opts = opts.to_hash
+ elsif opts.respond_to? :to_h
+ opts = opts.to_h
+ else
+ raise TypeError, "can't convert #{opts.class} into Hash"
+ end
end
- state.configure(opts)
+ options = options.merge(opts)
end
- state.generate(obj)
+
+ State.generate(obj, options, nil)
end
- # :stopdoc:
- # I want to deprecate these later, so I'll first be silent about them, and later delete them.
- alias pretty_unparse pretty_generate
- module_function :pretty_unparse
- # :startdoc:
+ # Sets or returns default options for the JSON.unsafe_load method.
+ # Initially:
+ # opts = JSON.load_default_options
+ # opts # => {:max_nesting=>false, :allow_nan=>true, :allow_blank=>true, :create_additions=>true}
+ deprecated_singleton_attr_accessor :unsafe_load_default_options
- class << self
- # The global default options for the JSON.load method:
- # :max_nesting: false
- # :allow_nan: true
- # :allow_blank: true
- attr_accessor :load_default_options
- end
- self.load_default_options = {
+ @unsafe_load_default_options = {
:max_nesting => false,
:allow_nan => true,
- :allow_blank => true,
+ :allow_blank => true,
:create_additions => true,
}
- # Load a ruby data structure from a JSON _source_ and return it. A source can
- # either be a string-like object, an IO-like object, or an object responding
- # to the read method. If _proc_ was given, it will be called with any nested
- # Ruby object as an argument recursively in depth first order. To modify the
- # default options pass in the optional _options_ argument as well.
+ # Sets or returns default options for the JSON.load method.
+ # Initially:
+ # opts = JSON.load_default_options
+ # opts # => {:max_nesting=>false, :allow_nan=>true, :allow_blank=>true, :create_additions=>true}
+ deprecated_singleton_attr_accessor :load_default_options
+
+ @load_default_options = {
+ :allow_nan => true,
+ :allow_blank => true,
+ :create_additions => nil,
+ }
+ # :call-seq:
+ # JSON.unsafe_load(source, options = {}) -> object
+ # JSON.unsafe_load(source, proc = nil, options = {}) -> object
+ #
+ # Returns the Ruby objects created by parsing the given +source+.
#
# BEWARE: This method is meant to serialise data from trusted user input,
# like from your own database server or clients under your control, it could
- # be dangerous to allow untrusted users to pass JSON sources into it. The
- # default options for the parser can be changed via the load_default_options
- # method.
- #
- # This method is part of the implementation of the load/dump interface of
- # Marshal and YAML.
- def load(source, proc = nil, options = {})
- opts = load_default_options.merge options
- if source.respond_to? :to_str
- source = source.to_str
- elsif source.respond_to? :to_io
- source = source.to_io.read
- elsif source.respond_to?(:read)
- source = source.read
+ # be dangerous to allow untrusted users to pass JSON sources into it.
+ #
+ # - Argument +source+ must be, or be convertible to, a \String:
+ # - If +source+ responds to instance method +to_str+,
+ # <tt>source.to_str</tt> becomes the source.
+ # - If +source+ responds to instance method +to_io+,
+ # <tt>source.to_io.read</tt> becomes the source.
+ # - If +source+ responds to instance method +read+,
+ # <tt>source.read</tt> becomes the source.
+ # - If both of the following are true, source becomes the \String <tt>'null'</tt>:
+ # - Option +allow_blank+ specifies a truthy value.
+ # - The source, as defined above, is +nil+ or the empty \String <tt>''</tt>.
+ # - Otherwise, +source+ remains the source.
+ # - Argument +proc+, if given, must be a \Proc that accepts one argument.
+ # It will be called recursively with each result (depth-first order).
+ # See details below.
+ # - Argument +opts+, if given, contains a \Hash of options for the parsing.
+ # See {Parsing Options}[#module-JSON-label-Parsing+Options].
+ # The default options can be changed via method JSON.unsafe_load_default_options=.
+ #
+ # ---
+ #
+ # When no +proc+ is given, modifies +source+ as above and returns the result of
+ # <tt>parse(source, opts)</tt>; see #parse.
+ #
+ # Source for following examples:
+ # source = <<~JSON
+ # {
+ # "name": "Dave",
+ # "age" :40,
+ # "hats": [
+ # "Cattleman's",
+ # "Panama",
+ # "Tophat"
+ # ]
+ # }
+ # JSON
+ #
+ # Load a \String:
+ # ruby = JSON.unsafe_load(source)
+ # ruby # => {"name"=>"Dave", "age"=>40, "hats"=>["Cattleman's", "Panama", "Tophat"]}
+ #
+ # Load an \IO object:
+ # require 'stringio'
+ # object = JSON.unsafe_load(StringIO.new(source))
+ # object # => {"name"=>"Dave", "age"=>40, "hats"=>["Cattleman's", "Panama", "Tophat"]}
+ #
+ # Load a \File object:
+ # path = 't.json'
+ # File.write(path, source)
+ # File.open(path) do |file|
+ # JSON.unsafe_load(file)
+ # end # => {"name"=>"Dave", "age"=>40, "hats"=>["Cattleman's", "Panama", "Tophat"]}
+ #
+ # ---
+ #
+ # When +proc+ is given:
+ # - Modifies +source+ as above.
+ # - Gets the +result+ from calling <tt>parse(source, opts)</tt>.
+ # - Recursively calls <tt>proc(result)</tt>.
+ # - Returns the final result.
+ #
+ # Example:
+ # require 'json'
+ #
+ # # Some classes for the example.
+ # class Base
+ # def initialize(attributes)
+ # @attributes = attributes
+ # end
+ # end
+ # class User < Base; end
+ # class Account < Base; end
+ # class Admin < Base; end
+ # # The JSON source.
+ # json = <<-EOF
+ # {
+ # "users": [
+ # {"type": "User", "username": "jane", "email": "jane@example.com"},
+ # {"type": "User", "username": "john", "email": "john@example.com"}
+ # ],
+ # "accounts": [
+ # {"account": {"type": "Account", "paid": true, "account_id": "1234"}},
+ # {"account": {"type": "Account", "paid": false, "account_id": "1235"}}
+ # ],
+ # "admins": {"type": "Admin", "password": "0wn3d"}
+ # }
+ # EOF
+ # # Deserializer method.
+ # def deserialize_obj(obj, safe_types = %w(User Account Admin))
+ # type = obj.is_a?(Hash) && obj["type"]
+ # safe_types.include?(type) ? Object.const_get(type).new(obj) : obj
+ # end
+ # # Call to JSON.unsafe_load
+ # ruby = JSON.unsafe_load(json, proc {|obj|
+ # case obj
+ # when Hash
+ # obj.each {|k, v| obj[k] = deserialize_obj v }
+ # when Array
+ # obj.map! {|v| deserialize_obj v }
+ # end
+ # obj
+ # })
+ # pp ruby
+ # Output:
+ # {"users"=>
+ # [#<User:0x00000000064c4c98
+ # @attributes=
+ # {"type"=>"User", "username"=>"jane", "email"=>"jane@example.com"}>,
+ # #<User:0x00000000064c4bd0
+ # @attributes=
+ # {"type"=>"User", "username"=>"john", "email"=>"john@example.com"}>],
+ # "accounts"=>
+ # [{"account"=>
+ # #<Account:0x00000000064c4928
+ # @attributes={"type"=>"Account", "paid"=>true, "account_id"=>"1234"}>},
+ # {"account"=>
+ # #<Account:0x00000000064c4680
+ # @attributes={"type"=>"Account", "paid"=>false, "account_id"=>"1235"}>}],
+ # "admins"=>
+ # #<Admin:0x00000000064c41f8
+ # @attributes={"type"=>"Admin", "password"=>"0wn3d"}>}
+ #
+ def unsafe_load(source, proc = nil, options = nil)
+ opts = if options.nil?
+ if proc && proc.is_a?(Hash)
+ options, proc = proc, nil
+ options
+ else
+ _unsafe_load_default_options
+ end
+ else
+ _unsafe_load_default_options.merge(options)
end
+
+ unless source.is_a?(String)
+ if source.respond_to? :to_str
+ source = source.to_str
+ elsif source.respond_to? :to_io
+ source = source.to_io.read
+ elsif source.respond_to?(:read)
+ source = source.read
+ end
+ end
+
if opts[:allow_blank] && (source.nil? || source.empty?)
source = 'null'
end
- result = parse(source, opts)
- recurse_proc(result, &proc) if proc
- result
+
+ if proc
+ opts = opts.dup
+ opts[:on_load] = proc.to_proc
+ end
+
+ parse(source, opts)
end
- # Recursively calls passed _Proc_ if the parsed data structure is an _Array_ or _Hash_
- def recurse_proc(result, &proc)
- case result
- when Array
- result.each { |x| recurse_proc x, &proc }
- proc.call result
- when Hash
- result.each { |x, y| recurse_proc x, &proc; recurse_proc y, &proc }
- proc.call result
+ # :call-seq:
+ # JSON.load(source, options = {}) -> object
+ # JSON.load(source, proc = nil, options = {}) -> object
+ #
+ # Returns the Ruby objects created by parsing the given +source+.
+ #
+ # BEWARE: This method is meant to serialise data from trusted user input,
+ # like from your own database server or clients under your control, it could
+ # be dangerous to allow untrusted users to pass JSON sources into it.
+ # If you must use it, use JSON.unsafe_load instead to make it clear.
+ #
+ # Since JSON version 2.8.0, `load` emits a deprecation warning when a
+ # non native type is deserialized, without `create_additions` being explicitly
+ # enabled, and in JSON version 3.0, `load` will have `create_additions` disabled
+ # by default.
+ #
+ # - Argument +source+ must be, or be convertible to, a \String:
+ # - If +source+ responds to instance method +to_str+,
+ # <tt>source.to_str</tt> becomes the source.
+ # - If +source+ responds to instance method +to_io+,
+ # <tt>source.to_io.read</tt> becomes the source.
+ # - If +source+ responds to instance method +read+,
+ # <tt>source.read</tt> becomes the source.
+ # - If both of the following are true, source becomes the \String <tt>'null'</tt>:
+ # - Option +allow_blank+ specifies a truthy value.
+ # - The source, as defined above, is +nil+ or the empty \String <tt>''</tt>.
+ # - Otherwise, +source+ remains the source.
+ # - Argument +proc+, if given, must be a \Proc that accepts one argument.
+ # It will be called recursively with each result (depth-first order).
+ # See details below.
+ # - Argument +opts+, if given, contains a \Hash of options for the parsing.
+ # See {Parsing Options}[#module-JSON-label-Parsing+Options].
+ # The default options can be changed via method JSON.load_default_options=.
+ #
+ # ---
+ #
+ # When no +proc+ is given, modifies +source+ as above and returns the result of
+ # <tt>parse(source, opts)</tt>; see #parse.
+ #
+ # Source for following examples:
+ # source = <<~JSON
+ # {
+ # "name": "Dave",
+ # "age" :40,
+ # "hats": [
+ # "Cattleman's",
+ # "Panama",
+ # "Tophat"
+ # ]
+ # }
+ # JSON
+ #
+ # Load a \String:
+ # ruby = JSON.load(source)
+ # ruby # => {"name"=>"Dave", "age"=>40, "hats"=>["Cattleman's", "Panama", "Tophat"]}
+ #
+ # Load an \IO object:
+ # require 'stringio'
+ # object = JSON.load(StringIO.new(source))
+ # object # => {"name"=>"Dave", "age"=>40, "hats"=>["Cattleman's", "Panama", "Tophat"]}
+ #
+ # Load a \File object:
+ # path = 't.json'
+ # File.write(path, source)
+ # File.open(path) do |file|
+ # JSON.load(file)
+ # end # => {"name"=>"Dave", "age"=>40, "hats"=>["Cattleman's", "Panama", "Tophat"]}
+ #
+ # ---
+ #
+ # When +proc+ is given:
+ # - Modifies +source+ as above.
+ # - Gets the +result+ from calling <tt>parse(source, opts)</tt>.
+ # - Recursively calls <tt>proc(result)</tt>.
+ # - Returns the final result.
+ #
+ # Example:
+ # require 'json'
+ #
+ # # Some classes for the example.
+ # class Base
+ # def initialize(attributes)
+ # @attributes = attributes
+ # end
+ # end
+ # class User < Base; end
+ # class Account < Base; end
+ # class Admin < Base; end
+ # # The JSON source.
+ # json = <<-EOF
+ # {
+ # "users": [
+ # {"type": "User", "username": "jane", "email": "jane@example.com"},
+ # {"type": "User", "username": "john", "email": "john@example.com"}
+ # ],
+ # "accounts": [
+ # {"account": {"type": "Account", "paid": true, "account_id": "1234"}},
+ # {"account": {"type": "Account", "paid": false, "account_id": "1235"}}
+ # ],
+ # "admins": {"type": "Admin", "password": "0wn3d"}
+ # }
+ # EOF
+ # # Deserializer method.
+ # def deserialize_obj(obj, safe_types = %w(User Account Admin))
+ # type = obj.is_a?(Hash) && obj["type"]
+ # safe_types.include?(type) ? Object.const_get(type).new(obj) : obj
+ # end
+ # # Call to JSON.load
+ # ruby = JSON.load(json, proc {|obj|
+ # case obj
+ # when Hash
+ # obj.each {|k, v| obj[k] = deserialize_obj v }
+ # when Array
+ # obj.map! {|v| deserialize_obj v }
+ # end
+ # obj
+ # })
+ # pp ruby
+ # Output:
+ # {"users"=>
+ # [#<User:0x00000000064c4c98
+ # @attributes=
+ # {"type"=>"User", "username"=>"jane", "email"=>"jane@example.com"}>,
+ # #<User:0x00000000064c4bd0
+ # @attributes=
+ # {"type"=>"User", "username"=>"john", "email"=>"john@example.com"}>],
+ # "accounts"=>
+ # [{"account"=>
+ # #<Account:0x00000000064c4928
+ # @attributes={"type"=>"Account", "paid"=>true, "account_id"=>"1234"}>},
+ # {"account"=>
+ # #<Account:0x00000000064c4680
+ # @attributes={"type"=>"Account", "paid"=>false, "account_id"=>"1235"}>}],
+ # "admins"=>
+ # #<Admin:0x00000000064c41f8
+ # @attributes={"type"=>"Admin", "password"=>"0wn3d"}>}
+ #
+ def load(source, proc = nil, options = nil)
+ if proc && options.nil? && proc.is_a?(Hash)
+ options = proc
+ proc = nil
+ end
+
+ opts = if options.nil?
+ if proc && proc.is_a?(Hash)
+ options, proc = proc, nil
+ options
+ else
+ _load_default_options
+ end
else
- proc.call result
+ _load_default_options.merge(options)
end
- end
- alias restore load
- module_function :restore
+ unless source.is_a?(String)
+ if source.respond_to? :to_str
+ source = source.to_str
+ elsif source.respond_to? :to_io
+ source = source.to_io.read
+ elsif source.respond_to?(:read)
+ source = source.read
+ end
+ end
- class << self
- # The global default options for the JSON.dump method:
- # :max_nesting: false
- # :allow_nan: true
- # :allow_blank: true
- attr_accessor :dump_default_options
+ if opts[:allow_blank] && (source.nil? || (String === source && source.empty?))
+ source = 'null'
+ end
+
+ if proc
+ opts = opts.dup
+ opts[:on_load] = proc.to_proc
+ end
+
+ parse(source, opts)
end
- self.dump_default_options = {
+
+ # Sets or returns the default options for the JSON.dump method.
+ # Initially:
+ # opts = JSON.dump_default_options
+ # opts # => {:max_nesting=>false, :allow_nan=>true}
+ deprecated_singleton_attr_accessor :dump_default_options
+ @dump_default_options = {
:max_nesting => false,
:allow_nan => true,
}
- # Dumps _obj_ as a JSON string, i.e. calls generate on the object and returns
- # the result.
+ # :call-seq:
+ # JSON.dump(obj, io = nil, limit = nil)
+ #
+ # Dumps +obj+ as a \JSON string, i.e. calls generate on the object and returns the result.
#
- # If anIO (an IO-like object or an object that responds to the write method)
- # was given, the resulting JSON is written to it.
+ # The default options can be changed via method JSON.dump_default_options.
#
- # If the number of nested arrays or objects exceeds _limit_, an ArgumentError
- # exception is raised. This argument is similar (but not exactly the
- # same!) to the _limit_ argument in Marshal.dump.
+ # - Argument +io+, if given, should respond to method +write+;
+ # the \JSON \String is written to +io+, and +io+ is returned.
+ # If +io+ is not given, the \JSON \String is returned.
+ # - Argument +limit+, if given, is passed to JSON.generate as option +max_nesting+.
#
- # The default options for the generator can be changed via the
- # dump_default_options method.
+ # ---
#
- # This method is part of the implementation of the load/dump interface of
- # Marshal and YAML.
- def dump(obj, anIO = nil, limit = nil)
- if anIO and limit.nil?
- anIO = anIO.to_io if anIO.respond_to?(:to_io)
- unless anIO.respond_to?(:write)
- limit = anIO
- anIO = nil
+ # When argument +io+ is not given, returns the \JSON \String generated from +obj+:
+ # obj = {foo: [0, 1], bar: {baz: 2, bat: 3}, bam: :bad}
+ # json = JSON.dump(obj)
+ # json # => "{\"foo\":[0,1],\"bar\":{\"baz\":2,\"bat\":3},\"bam\":\"bad\"}"
+ #
+ # When argument +io+ is given, writes the \JSON \String to +io+ and returns +io+:
+ # path = 't.json'
+ # File.open(path, 'w') do |file|
+ # JSON.dump(obj, file)
+ # end # => #<File:t.json (closed)>
+ # puts File.read(path)
+ # Output:
+ # {"foo":[0,1],"bar":{"baz":2,"bat":3},"bam":"bad"}
+ def dump(obj, anIO = nil, limit = nil, kwargs = nil)
+ if kwargs.nil?
+ if limit.nil?
+ if anIO.is_a?(Hash)
+ kwargs = anIO
+ anIO = nil
+ end
+ elsif limit.is_a?(Hash)
+ kwargs = limit
+ limit = nil
end
end
- opts = JSON.dump_default_options
+
+ unless anIO.nil?
+ if anIO.respond_to?(:to_io)
+ anIO = anIO.to_io
+ elsif limit.nil? && !anIO.respond_to?(:write)
+ anIO, limit = nil, anIO
+ end
+ end
+
+ opts = JSON._dump_default_options
opts = opts.merge(:max_nesting => limit) if limit
- result = generate(obj, opts)
- if anIO
- anIO.write result
- anIO
+ opts = opts.merge(kwargs) if kwargs
+
+ begin
+ State.generate(obj, opts, anIO)
+ rescue JSON::NestingError
+ raise ArgumentError, "exceed depth limit"
+ end
+ end
+
+ # :stopdoc:
+ # All these were meant to be deprecated circa 2009, but were just set as undocumented
+ # so usage still exist in the wild.
+ def unparse(...)
+ if RUBY_VERSION >= "3.0"
+ warn "JSON.unparse is deprecated and will be removed in json 3.0.0, just use JSON.generate", uplevel: 1, category: :deprecated
+ else
+ warn "JSON.unparse is deprecated and will be removed in json 3.0.0, just use JSON.generate", uplevel: 1
+ end
+ generate(...)
+ end
+ module_function :unparse
+
+ def fast_unparse(...)
+ if RUBY_VERSION >= "3.0"
+ warn "JSON.fast_unparse is deprecated and will be removed in json 3.0.0, just use JSON.generate", uplevel: 1, category: :deprecated
+ else
+ warn "JSON.fast_unparse is deprecated and will be removed in json 3.0.0, just use JSON.generate", uplevel: 1
+ end
+ generate(...)
+ end
+ module_function :fast_unparse
+
+ def pretty_unparse(...)
+ if RUBY_VERSION >= "3.0"
+ warn "JSON.pretty_unparse is deprecated and will be removed in json 3.0.0, just use JSON.pretty_generate", uplevel: 1, category: :deprecated
else
- result
+ warn "JSON.pretty_unparse is deprecated and will be removed in json 3.0.0, just use JSON.pretty_generate", uplevel: 1
+ end
+ pretty_generate(...)
+ end
+ module_function :fast_unparse
+
+ def restore(...)
+ if RUBY_VERSION >= "3.0"
+ warn "JSON.restore is deprecated and will be removed in json 3.0.0, just use JSON.load", uplevel: 1, category: :deprecated
+ else
+ warn "JSON.restore is deprecated and will be removed in json 3.0.0, just use JSON.load", uplevel: 1
+ end
+ load(...)
+ end
+ module_function :restore
+
+ class << self
+ private
+
+ def const_missing(const_name)
+ case const_name
+ when :PRETTY_STATE_PROTOTYPE
+ if RUBY_VERSION >= "3.0"
+ warn "JSON::PRETTY_STATE_PROTOTYPE is deprecated and will be removed in json 3.0.0, just use JSON.pretty_generate", uplevel: 1, category: :deprecated
+ else
+ warn "JSON::PRETTY_STATE_PROTOTYPE is deprecated and will be removed in json 3.0.0, just use JSON.pretty_generate", uplevel: 1
+ end
+ state.new(PRETTY_GENERATE_OPTIONS)
+ else
+ super
+ end
+ end
+ end
+ # :startdoc:
+
+ # JSON::Coder holds a parser and generator configuration.
+ #
+ # module MyApp
+ # JSONC_CODER = JSON::Coder.new(
+ # allow_trailing_comma: true
+ # )
+ # end
+ #
+ # MyApp::JSONC_CODER.load(document)
+ #
+ class Coder
+ # :call-seq:
+ # JSON.new(options = nil, &block)
+ #
+ # Argument +options+, if given, contains a \Hash of options for both parsing and generating.
+ # See {Parsing Options}[rdoc-ref:JSON@Parsing+Options],
+ # and {Generating Options}[rdoc-ref:JSON@Generating+Options].
+ #
+ # For generation, the <tt>strict: true</tt> option is always set. When a Ruby object with no native \JSON counterpart is
+ # encountered, the block provided to the initialize method is invoked, and must return a Ruby object that has a native
+ # \JSON counterpart:
+ #
+ # module MyApp
+ # API_JSON_CODER = JSON::Coder.new do |object|
+ # case object
+ # when Time
+ # object.iso8601(3)
+ # else
+ # object # Unknown type, will raise
+ # end
+ # end
+ # end
+ #
+ # puts MyApp::API_JSON_CODER.dump(Time.now.utc) # => "2025-01-21T08:41:44.286Z"
+ #
+ def initialize(options = nil, &as_json)
+ if options.nil?
+ options = { strict: true }
+ else
+ options = options.dup
+ options[:strict] = true
+ end
+ options[:as_json] = as_json if as_json
+
+ @state = State.new(options).freeze
+ @parser_config = Ext::Parser::Config.new(ParserOptions.prepare(options)).freeze
+ end
+
+ # call-seq:
+ # dump(object) -> String
+ # dump(object, io) -> io
+ #
+ # Serialize the given object into a \JSON document.
+ def dump(object, io = nil)
+ @state.generate(object, io)
+ end
+ alias_method :generate, :dump
+
+ # call-seq:
+ # load(string) -> Object
+ #
+ # Parse the given \JSON document and return an equivalent Ruby object.
+ def load(source)
+ @parser_config.parse(source)
+ end
+ alias_method :parse, :load
+
+ # call-seq:
+ # load(path) -> Object
+ #
+ # Parse the given \JSON document and return an equivalent Ruby object.
+ def load_file(path)
+ load(File.read(path, encoding: Encoding::UTF_8))
end
- rescue JSON::NestingError
- raise ArgumentError, "exceed depth limit"
end
- # Encodes string using Ruby's _String.encode_
- def self.iconv(to, from, string)
- string.encode(to, from)
+ module GeneratorMethods
+ # call-seq: to_json(*)
+ #
+ # Converts this object into a JSON string.
+ # If this object doesn't directly maps to a JSON native type,
+ # first convert it to a string (calling #to_s), then converts
+ # it to a JSON string, and returns the result.
+ # This is a fallback, if no special method #to_json was defined for some object.
+ def to_json(state = nil, *)
+ obj = case self
+ when nil, false, true, Integer, Float, Array, Hash
+ self
+ else
+ "#{self}"
+ end
+
+ if state.nil?
+ JSON::State._generate_no_fallback(obj, nil, nil)
+ else
+ JSON::State.from_state(state)._generate_no_fallback(obj)
+ end
+ end
end
end
@@ -414,8 +1130,14 @@ module ::Kernel
# Outputs _objs_ to STDOUT as JSON strings in the shortest form, that is in
# one line.
def j(*objs)
+ if RUBY_VERSION >= "3.0"
+ warn "Kernel#j is deprecated and will be removed in json 3.0.0", uplevel: 1, category: :deprecated
+ else
+ warn "Kernel#j is deprecated and will be removed in json 3.0.0", uplevel: 1
+ end
+
objs.each do |obj|
- puts JSON::generate(obj, :allow_nan => true, :max_nesting => false)
+ puts JSON.generate(obj, :allow_nan => true, :max_nesting => false)
end
nil
end
@@ -423,8 +1145,14 @@ module ::Kernel
# Outputs _objs_ to STDOUT as JSON strings in a pretty format, with
# indentation and over many lines.
def jj(*objs)
+ if RUBY_VERSION >= "3.0"
+ warn "Kernel#jj is deprecated and will be removed in json 3.0.0", uplevel: 1, category: :deprecated
+ else
+ warn "Kernel#jj is deprecated and will be removed in json 3.0.0", uplevel: 1
+ end
+
objs.each do |obj|
- puts JSON::pretty_generate(obj, :allow_nan => true, :max_nesting => false)
+ puts JSON.pretty_generate(obj, :allow_nan => true, :max_nesting => false)
end
nil
end
@@ -435,22 +1163,11 @@ module ::Kernel
#
# The _opts_ argument is passed through to generate/parse respectively. See
# generate and parse for their documentation.
- def JSON(object, *args)
- if object.respond_to? :to_str
- JSON.parse(object.to_str, args.first)
- else
- JSON.generate(object, args.first)
- end
+ def JSON(object, opts = nil)
+ JSON[object, opts]
end
end
-# Extends any Class to include _json_creatable?_ method.
-class ::Class
- # Returns true if this class can be used to create an instance
- # from a serialised JSON string. The class has to implement a class
- # method _json_create_ that expects a hash as first parameter. The hash
- # should include the required data.
- def json_creatable?
- respond_to?(:json_create)
- end
+class Object
+ include JSON::GeneratorMethods
end
diff --git a/ext/json/lib/json/ext.rb b/ext/json/lib/json/ext.rb
index 7264a857fa..5bacc5e371 100644
--- a/ext/json/lib/json/ext.rb
+++ b/ext/json/lib/json/ext.rb
@@ -1,15 +1,45 @@
+# frozen_string_literal: true
+
require 'json/common'
module JSON
# This module holds all the modules/classes that implement JSON's
# functionality as C extensions.
module Ext
+ class Parser
+ class << self
+ def parse(...)
+ new(...).parse
+ end
+ alias_method :parse, :parse # Allow redefinition by extensions
+ end
+
+ def initialize(source, opts = nil)
+ @source = source
+ @config = Config.new(opts)
+ end
+
+ def source
+ @source.dup
+ end
+
+ def parse
+ @config.parse(@source)
+ end
+ end
+
require 'json/ext/parser'
- require 'json/ext/generator'
- $DEBUG and warn "Using Ext extension for JSON."
- JSON.parser = Parser
- JSON.generator = Generator
+ Ext::Parser::Config = Ext::ParserConfig
+ JSON.parser = Ext::Parser
+
+ if RUBY_ENGINE == 'truffleruby'
+ require 'json/truffle_ruby/generator'
+ JSON.generator = JSON::TruffleRuby::Generator
+ else
+ require 'json/ext/generator'
+ JSON.generator = Generator
+ end
end
- JSON_LOADED = true unless defined?(::JSON::JSON_LOADED)
+ JSON_LOADED = true unless defined?(JSON::JSON_LOADED)
end
diff --git a/ext/json/lib/json/ext/generator/state.rb b/ext/json/lib/json/ext/generator/state.rb
new file mode 100644
index 0000000000..e4f425af6a
--- /dev/null
+++ b/ext/json/lib/json/ext/generator/state.rb
@@ -0,0 +1,103 @@
+# frozen_string_literal: true
+
+module JSON
+ module Ext
+ module Generator
+ class State
+ # call-seq: new(opts = {})
+ #
+ # Instantiates a new State object, configured by _opts_.
+ #
+ # Argument +opts+, if given, contains a \Hash of options for the generation.
+ # See {Generating Options}[rdoc-ref:JSON@Generating+Options].
+ def initialize(opts = nil)
+ if opts && !opts.empty?
+ configure(opts)
+ end
+ end
+
+ # call-seq: configure(opts)
+ #
+ # Configure this State instance with the Hash _opts_, and return
+ # itself.
+ def configure(opts)
+ unless opts.is_a?(Hash)
+ if opts.respond_to?(:to_hash)
+ opts = opts.to_hash
+ elsif opts.respond_to?(:to_h)
+ opts = opts.to_h
+ else
+ raise TypeError, "can't convert #{opts.class} into Hash"
+ end
+ end
+ _configure(opts)
+ end
+
+ alias_method :merge, :configure
+
+ # call-seq: to_h
+ #
+ # Returns the configuration instance variables as a hash, that can be
+ # passed to the configure method.
+ def to_h
+ result = {
+ indent: indent,
+ space: space,
+ space_before: space_before,
+ object_nl: object_nl,
+ array_nl: array_nl,
+ as_json: as_json,
+ allow_nan: allow_nan?,
+ ascii_only: ascii_only?,
+ max_nesting: max_nesting,
+ script_safe: script_safe?,
+ strict: strict?,
+ depth: depth,
+ buffer_initial_length: buffer_initial_length,
+ }
+
+ allow_duplicate_key = allow_duplicate_key?
+ unless allow_duplicate_key.nil?
+ result[:allow_duplicate_key] = allow_duplicate_key
+ end
+
+ instance_variables.each do |iv|
+ iv = iv.to_s[1..-1]
+ result[iv.to_sym] = self[iv]
+ end
+
+ result
+ end
+
+ alias_method :to_hash, :to_h
+
+ # call-seq: [](name)
+ #
+ # Returns the value returned by method +name+.
+ def [](name)
+ ::JSON.deprecation_warning("JSON::State#[] is deprecated and will be removed in json 3.0.0")
+
+ if respond_to?(name)
+ __send__(name)
+ else
+ instance_variable_get("@#{name}") if
+ instance_variables.include?("@#{name}".to_sym) # avoid warning
+ end
+ end
+
+ # call-seq: []=(name, value)
+ #
+ # Sets the attribute name to value.
+ def []=(name, value)
+ ::JSON.deprecation_warning("JSON::State#[]= is deprecated and will be removed in json 3.0.0")
+
+ if respond_to?(name_writer = "#{name}=")
+ __send__ name_writer, value
+ else
+ instance_variable_set "@#{name}", value
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/ext/json/lib/json/generic_object.rb b/ext/json/lib/json/generic_object.rb
index 108309db26..5c8ace354b 100644
--- a/ext/json/lib/json/generic_object.rb
+++ b/ext/json/lib/json/generic_object.rb
@@ -1,5 +1,9 @@
-#frozen_string_literal: false
-require 'ostruct'
+# frozen_string_literal: true
+begin
+ require 'ostruct'
+rescue LoadError
+ warn "JSON::GenericObject requires 'ostruct'. Please install it with `gem install ostruct`."
+end
module JSON
class GenericObject < OpenStruct
@@ -48,14 +52,6 @@ module JSON
table
end
- def [](name)
- __send__(name)
- end unless method_defined?(:[])
-
- def []=(name, value)
- __send__("#{name}=", value)
- end unless method_defined?(:[]=)
-
def |(other)
self.class[other.to_hash.merge(to_hash)]
end
@@ -67,5 +63,5 @@ module JSON
def to_json(*a)
as_json.to_json(*a)
end
- end
+ end if defined?(::OpenStruct)
end
diff --git a/ext/json/lib/json/version.rb b/ext/json/lib/json/version.rb
index 9d781df875..a69590ff9c 100644
--- a/ext/json/lib/json/version.rb
+++ b/ext/json/lib/json/version.rb
@@ -1,9 +1,5 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
+
module JSON
- # JSON version
- VERSION = '2.3.0'
- VERSION_ARRAY = VERSION.split(/\./).map { |x| x.to_i } # :nodoc:
- VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc:
- VERSION_MINOR = VERSION_ARRAY[1] # :nodoc:
- VERSION_BUILD = VERSION_ARRAY[2] # :nodoc:
+ VERSION = '2.19.7'
end