Consider Symbol "JSON-ready", improve jsonify

Previously jsonify would call `.as_json` for Integer, nil, true, and
false, even though those types are considered "JSON-ready". Technically
a user could have overridden `.as_json` for these types but I can't
imagine a use case and I don't think we should support that.

I left the same behaviour of calling `.as_json` for generic "Numeric" as
that can have user subclasses where one may have implemented as_json.
This behaviour is also used for Float (which coerces
NaN/Infinity/-Infinity into nil).

This also adds Symbol to the list of "JSON-ready" types, to avoid
unnecessarily casting them to strings (possible as we no longer perform
escaping on input). The output of jsonify should never be user visible
before it is passed through JSON.generate, so I don't think this should
be a user facing
change.

This also corrects our handling of Hash to call to_s on all keys,
matching the behaviour of `.as_json` and JSON's requirement that keys
are Strings (Symbols are also permitted as JSON knows to convert them to
a String).
This commit is contained in:
John Hawthorn 2023-06-29 14:12:00 -07:00
parent 52be530755
commit ab01f9f3da
2 changed files with 29 additions and 5 deletions

View File

@ -67,7 +67,7 @@ module ActiveSupport
:ESCAPE_REGEX_WITHOUT_HTML_ENTITIES
# Convert an object into a "JSON-ready" representation composed of
# primitives like Hash, Array, String, Numeric,
# primitives like Hash, Array, String, Symbol, Numeric,
# and +true+/+false+/+nil+.
# Recursively calls #as_json to the object to recursively build a
# fully JSON-ready object.
@ -81,14 +81,15 @@ module ActiveSupport
# calls.
def jsonify(value)
case value
when String
when String, Integer, Symbol, nil, true, false
value
when Numeric, NilClass, TrueClass, FalseClass
when Numeric
value.as_json
when Hash
result = {}
value.each do |k, v|
result[jsonify(k)] = jsonify(v)
k = k.to_s unless Symbol === k || String === k
result[k] = jsonify(v)
end
result
when Array

View File

@ -36,6 +36,26 @@ module JSONTest
end
end
class RomanNumeral < Numeric
def initialize(str)
@str = str
end
def as_json(options = nil)
@str
end
end
class CustomNumeric < Numeric
def initialize(str)
@str = str
end
def to_json(options = nil)
@str
end
end
module EncodingTestCases
TrueTests = [[ true, %(true) ]]
FalseTests = [[ false, %(false) ]]
@ -46,7 +66,10 @@ module JSONTest
[ 1.0 / 0.0, %(null) ],
[ -1.0 / 0.0, %(null) ],
[ BigDecimal("0.0") / BigDecimal("0.0"), %(null) ],
[ BigDecimal("2.5"), %("#{BigDecimal('2.5')}") ]]
[ BigDecimal("2.5"), %("#{BigDecimal('2.5')}") ],
[ RomanNumeral.new("MCCCXXXVII"), %("MCCCXXXVII") ],
[ [CustomNumeric.new("123")], %([123]) ]
]
StringTests = [[ "this is the <string>", %("this is the \\u003cstring\\u003e")],
[ 'a "string" with quotes & an ampersand', %("a \\"string\\" with quotes \\u0026 an ampersand") ],