Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve performance of ActiveSupport::JSON.encode #48614

Merged
merged 3 commits into from Jun 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -179,7 +179,7 @@ def @contact.favorite_quote; "Constraints are liberating"; end
end

test "custom as_json should be honored when generating json" do
def @contact.as_json(options); { name: name, created_at: created_at }; end
def @contact.as_json(options = nil); { name: name, created_at: created_at }; end
json = @contact.to_json

assert_match %r{"name":"Konata Izumi"}, json
Expand Down
4 changes: 2 additions & 2 deletions activerecord/test/cases/json_serialization_test.rb
Expand Up @@ -149,8 +149,8 @@ def test_serializable_hash_with_default_except_option_and_excluding_inheritance_
@contact = ContactSti.new(@contact.attributes)
assert_equal "ContactSti", @contact.type

def @contact.serializable_hash(options = {})
super({ except: %w(age) }.merge!(options))
def @contact.serializable_hash(options = nil)
super({ except: %w(age) }.merge!(options || {}))
end

json = @contact.to_json
Expand Down
43 changes: 17 additions & 26 deletions activesupport/lib/active_support/json/encoding.rb
Expand Up @@ -35,7 +35,16 @@ def initialize(options = nil)

# Encode the given object into a JSON string
def encode(value)
stringify jsonify value.as_json(options.dup)
unless options.empty?
value = value.as_json(options.dup)
end
json = stringify(jsonify(value))
if Encoding.escape_html_entities_in_json
json.gsub! ESCAPE_REGEX_WITH_HTML_ENTITIES, ESCAPED_CHARS
else
json.gsub! ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, ESCAPED_CHARS
end
json
end

private
Expand All @@ -53,31 +62,12 @@ def encode(value)
ESCAPE_REGEX_WITH_HTML_ENTITIES = /[\u2028\u2029><&]/u
ESCAPE_REGEX_WITHOUT_HTML_ENTITIES = /[\u2028\u2029]/u

# This class wraps all the strings we see and does the extra escaping
class EscapedString < String # :nodoc:
def to_json(*)
if Encoding.escape_html_entities_in_json
s = super
s.gsub! ESCAPE_REGEX_WITH_HTML_ENTITIES, ESCAPED_CHARS
s
else
s = super
s.gsub! ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, ESCAPED_CHARS
s
end
end

def to_s
self
end
end

# Mark these as private so we don't leak encoding-specific constructs
private_constant :ESCAPED_CHARS, :ESCAPE_REGEX_WITH_HTML_ENTITIES,
:ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, :EscapedString
: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.
Expand All @@ -91,14 +81,15 @@ def to_s
# calls.
def jsonify(value)
case value
when String
EscapedString.new(value)
when Numeric, NilClass, TrueClass, FalseClass
when String, Integer, Symbol, nil, true, false
value
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
Expand Down
25 changes: 24 additions & 1 deletion activesupport/test/json/encoding_test_cases.rb
Expand Up @@ -36,6 +36,26 @@ def initialize(*)
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) ]]
Expand All @@ -46,7 +66,10 @@ module EncodingTestCases
[ 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") ],
Expand Down