Skip to content

Instantly share code, notes, and snippets.

@abicky

abicky/Gemfile Secret

Last active May 7, 2017
Embed
What would you like to do?
Comparison of JSON serialization in Ruby
require 'open-uri'
require 'json'
require 'active_support/time'
require 'active_support/json'
require 'oj'
require 'benchmark/ips'
films = JSON.parse(open('https://raw.githubusercontent.com/apache/lucene-solr/releases/lucene-solr/6.5.1/solr/example/films/films.json').read)
Benchmark.ips do |x|
x.report('JSON.generate') { JSON.generate(films) }
x.report('JSON.fast_generate') { JSON.fast_generate(films) }
x.report('ActiveSupport::JSON.encode') { ActiveSupport::JSON.encode(films) }
x.report('Oj.dump (compat)') { Oj.dump(films, mode: :compat) }
x.report('Oj.dump (rails)') { Oj.dump(films, mode: :rails) }
x.compare!
end
# Warming up --------------------------------------
# JSON.generate 16.000 i/100ms
# JSON.fast_generate 15.000 i/100ms
# ActiveSupport::JSON.encode
# 1.000 i/100ms
# Oj.dump (compat) 57.000 i/100ms
# Oj.dump (rails) 59.000 i/100ms
# Calculating -------------------------------------
# JSON.generate 161.295 (± 9.3%) i/s - 800.000 in 5.015347s
# JSON.fast_generate 163.443 (± 9.2%) i/s - 810.000 in 5.001334s
# ActiveSupport::JSON.encode
# 14.141 (± 7.1%) i/s - 70.000 in 5.014242s
# Oj.dump (compat) 587.586 (±10.9%) i/s - 2.907k in 5.013221s
# Oj.dump (rails) 612.870 (±10.1%) i/s - 3.068k in 5.065790s
# Comparison:
# Oj.dump (rails): 612.9 i/s
# Oj.dump (compat): 587.6 i/s - same-ish: difference falls within error
# JSON.fast_generate: 163.4 i/s - 3.75x slower
# JSON.generate: 161.3 i/s - 3.80x slower
# ActiveSupport::JSON.encode: 14.1 i/s - 43.34x slower
diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb
index 6d8f7cf..cce73d8 100644
--- a/activesupport/test/json/encoding_test.rb
+++ b/activesupport/test/json/encoding_test.rb
@@ -28,7 +28,7 @@ def sorted_json(json)
ActiveSupport.escape_html_entities_in_json = !standard_class_tests
ActiveSupport.use_standard_json_time_format = standard_class_tests
JSONTest::EncodingTestCases.const_get(class_tests).each do |pair|
- assert_equal pair.last, sorted_json(ActiveSupport::JSON.encode(pair.first))
+ assert_equal pair.last, sorted_json(JSON.generate(pair.first))
end
ensure
ActiveSupport.escape_html_entities_in_json = false
@@ -43,79 +43,79 @@ def test_process_status
# There doesn't seem to be a good way to get a handle on a Process::Status object without actually
# creating a child process, hence this to populate $?
system("not_a_real_program_#{SecureRandom.hex}")
- assert_equal %({"exitstatus":#{$?.exitstatus},"pid":#{$?.pid}}), ActiveSupport::JSON.encode($?)
+ assert_equal %({"exitstatus":#{$?.exitstatus},"pid":#{$?.pid}}), JSON.generate($?)
end
def test_hash_encoding
- assert_equal %({\"a\":\"b\"}), ActiveSupport::JSON.encode(a: :b)
- assert_equal %({\"a\":1}), ActiveSupport::JSON.encode("a" => 1)
- assert_equal %({\"a\":[1,2]}), ActiveSupport::JSON.encode("a" => [1, 2])
- assert_equal %({"1":2}), ActiveSupport::JSON.encode(1 => 2)
+ assert_equal %({\"a\":\"b\"}), JSON.generate(a: :b)
+ assert_equal %({\"a\":1}), JSON.generate("a" => 1)
+ assert_equal %({\"a\":[1,2]}), JSON.generate("a" => [1, 2])
+ assert_equal %({"1":2}), JSON.generate(1 => 2)
- assert_equal %({\"a\":\"b\",\"c\":\"d\"}), sorted_json(ActiveSupport::JSON.encode(a: :b, c: :d))
+ assert_equal %({\"a\":\"b\",\"c\":\"d\"}), sorted_json(JSON.generate(a: :b, c: :d))
end
def test_hash_keys_encoding
ActiveSupport.escape_html_entities_in_json = true
- assert_equal "{\"\\u003c\\u003e\":\"\\u003c\\u003e\"}", ActiveSupport::JSON.encode("<>" => "<>")
+ assert_equal "{\"\\u003c\\u003e\":\"\\u003c\\u003e\"}", JSON.generate("<>" => "<>")
ensure
ActiveSupport.escape_html_entities_in_json = false
end
def test_utf8_string_encoded_properly
- result = ActiveSupport::JSON.encode("€2.99")
+ result = JSON.generate("€2.99")
assert_equal '"€2.99"', result
assert_equal(Encoding::UTF_8, result.encoding)
- result = ActiveSupport::JSON.encode("✎☺")
+ result = JSON.generate("✎☺")
assert_equal '"✎☺"', result
assert_equal(Encoding::UTF_8, result.encoding)
end
def test_non_utf8_string_transcodes
s = "二".encode("Shift_JIS")
- result = ActiveSupport::JSON.encode(s)
+ result = JSON.generate(s)
assert_equal '"二"', result
assert_equal Encoding::UTF_8, result.encoding
end
def test_wide_utf8_chars
w = "𠜎"
- result = ActiveSupport::JSON.encode(w)
+ result = JSON.generate(w)
assert_equal '"𠜎"', result
end
def test_wide_utf8_roundtrip
hash = { string: "𐒑" }
- json = ActiveSupport::JSON.encode(hash)
+ json = JSON.generate(hash)
decoded_hash = ActiveSupport::JSON.decode(json)
assert_equal "𐒑", decoded_hash["string"]
end
def test_hash_key_identifiers_are_always_quoted
values = { 0 => 0, 1 => 1, :_ => :_, "$" => "$", "a" => "a", :A => :A, :A0 => :A0, "A0B" => "A0B" }
- assert_equal %w( "$" "A" "A0" "A0B" "_" "a" "0" "1" ).sort, object_keys(ActiveSupport::JSON.encode(values))
+ assert_equal %w( "$" "A" "A0" "A0B" "_" "a" "0" "1" ).sort, object_keys(JSON.generate(values))
end
def test_hash_should_allow_key_filtering_with_only
- assert_equal %({"a":1}), ActiveSupport::JSON.encode({ "a" => 1, :b => 2, :c => 3 }, only: "a")
+ assert_equal %({"a":1}), JSON.generate({ "a" => 1, :b => 2, :c => 3 }, only: "a")
end
def test_hash_should_allow_key_filtering_with_except
- assert_equal %({"b":2}), ActiveSupport::JSON.encode({ "foo" => "bar", :b => 2, :c => 3 }, except: ["foo", :c])
+ assert_equal %({"b":2}), JSON.generate({ "foo" => "bar", :b => 2, :c => 3 }, except: ["foo", :c])
end
def test_time_to_json_includes_local_offset
with_standard_json_time_format(true) do
with_env_tz "US/Eastern" do
- assert_equal %("2005-02-01T15:15:10.000-05:00"), ActiveSupport::JSON.encode(Time.local(2005, 2, 1, 15, 15, 10))
+ assert_equal %("2005-02-01T15:15:10.000-05:00"), JSON.generate(Time.local(2005, 2, 1, 15, 15, 10))
end
end
end
def test_hash_with_time_to_json
with_standard_json_time_format(false) do
- assert_equal '{"time":"2009/01/01 00:00:00 +0000"}', { time: Time.utc(2009) }.to_json
+ assert_equal '{"time":"2009/01/01 00:00:00 +0000"}', JSON.generate({ time: Time.utc(2009) })
end
end
@@ -127,13 +127,13 @@ def test_nested_hash_with_float
latitude: 123.234
}
}
- ActiveSupport::JSON.encode(hash)
+ JSON.generate(hash)
end
end
def test_hash_like_with_options
h = JSONTest::Hashlike.new
- json = h.to_json only: [:foo]
+ json = JSON.generate(h, only: [:foo])
assert_equal({ "foo" => "hello" }, JSON.parse(json))
end
@@ -142,7 +142,7 @@ def test_object_to_json_with_options
obj = Object.new
obj.instance_variable_set :@foo, "hello"
obj.instance_variable_set :@bar, "world"
- json = obj.to_json only: ["foo"]
+ json = JSON.generate(obj, only: ["foo"])
assert_equal({ "foo" => "hello" }, JSON.parse(json))
end
@@ -151,7 +151,7 @@ def test_struct_to_json_with_options
struct = Struct.new(:foo, :bar).new
struct.foo = "hello"
struct.bar = "world"
- json = struct.to_json only: [:foo]
+ json = JSON.generate(struct, only: [:foo])
assert_equal({ "foo" => "hello" }, JSON.parse(json))
end
@@ -177,7 +177,7 @@ def test_hash_should_pass_encoding_options_to_children_in_to_json
country: "UK"
}
}
- json = person.to_json only: [:address, :city]
+ json = JSON.generate(person, only: [:address, :city])
assert_equal(%({"address":{"city":"London"}}), json)
end
@@ -201,7 +201,7 @@ def test_array_should_pass_encoding_options_to_children_in_to_json
{ name: "John", address: { city: "London", country: "UK" } },
{ name: "Jean", address: { city: "Paris" , country: "France" } }
]
- json = people.to_json only: [:address, :city]
+ json = JSON.generate(people, only: [:address, :city])
assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json)
end
@@ -233,7 +233,7 @@ def test_enumerable_should_generate_json_with_as_json
end
def test_enumerable_should_generate_json_with_to_json
- json = People.new.to_json only: [:address, :city]
+ json = JSON.generate(People.new, only: [:address, :city])
assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json)
end
@@ -248,7 +248,7 @@ def test_enumerable_should_pass_encoding_options_to_children_in_as_json
end
def test_enumerable_should_pass_encoding_options_to_children_in_to_json
- json = People.new.each.to_json only: [:address, :city]
+ json = JSON.generate(People.new.each, only: [:address, :city])
assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json)
end
@@ -269,7 +269,7 @@ def test_hash_to_json_should_not_keep_options_around
hash = { "foo" => f, "other_hash" => { "foo" => "other_foo", "test" => "other_test" } }
assert_equal({ "foo" => { "foo" => "hello", "bar" => "world" },
- "other_hash" => { "foo" => "other_foo", "test" => "other_test" } }, ActiveSupport::JSON.decode(hash.to_json))
+ "other_hash" => { "foo" => "other_foo", "test" => "other_test" } }, ActiveSupport::JSON.decode(JSON.generate(hash)))
end
def test_array_to_json_should_not_keep_options_around
@@ -279,7 +279,7 @@ def test_array_to_json_should_not_keep_options_around
array = [f, { "foo" => "other_foo", "test" => "other_test" }]
assert_equal([{ "foo" => "hello", "bar" => "world" },
- { "foo" => "other_foo", "test" => "other_test" }], ActiveSupport::JSON.decode(array.to_json))
+ { "foo" => "other_foo", "test" => "other_test" }], ActiveSupport::JSON.decode(JSON.generate(array)))
end
class OptionsTest
@@ -311,9 +311,9 @@ def test_struct_encoding
json_custom = ""
assert_nothing_raised do
- json_strings = user_email.to_json
- json_string_and_date = user_birthday.to_json
- json_custom = custom.to_json
+ json_strings = JSON.generate(user_email)
+ json_string_and_date = JSON.generate(user_birthday)
+ json_custom = JSON.generate(custom)
end
assert_equal({ "name" => "David",
@@ -383,7 +383,7 @@ def test_twz_to_json_with_use_standard_json_time_format_config_set_to_false
with_standard_json_time_format(false) do
zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
time = ActiveSupport::TimeWithZone.new(Time.utc(2000), zone)
- assert_equal "\"1999/12/31 19:00:00 -0500\"", ActiveSupport::JSON.encode(time)
+ assert_equal "\"1999/12/31 19:00:00 -0500\"", JSON.generate(time)
end
end
@@ -391,7 +391,7 @@ def test_twz_to_json_with_use_standard_json_time_format_config_set_to_true
with_standard_json_time_format(true) do
zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
time = ActiveSupport::TimeWithZone.new(Time.utc(2000), zone)
- assert_equal "\"1999-12-31T19:00:00.000-05:00\"", ActiveSupport::JSON.encode(time)
+ assert_equal "\"1999-12-31T19:00:00.000-05:00\"", JSON.generate(time)
end
end
@@ -400,7 +400,7 @@ def test_twz_to_json_with_custom_time_precision
with_time_precision(0) do
zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
time = ActiveSupport::TimeWithZone.new(Time.utc(2000), zone)
- assert_equal "\"1999-12-31T19:00:00-05:00\"", ActiveSupport::JSON.encode(time)
+ assert_equal "\"1999-12-31T19:00:00-05:00\"", JSON.generate(time)
end
end
end
@@ -408,7 +408,7 @@ def test_twz_to_json_with_custom_time_precision
def test_time_to_json_with_custom_time_precision
with_standard_json_time_format(true) do
with_time_precision(0) do
- assert_equal "\"2000-01-01T00:00:00Z\"", ActiveSupport::JSON.encode(Time.utc(2000))
+ assert_equal "\"2000-01-01T00:00:00Z\"", JSON.generate(Time.utc(2000))
end
end
end
@@ -416,7 +416,7 @@ def test_time_to_json_with_custom_time_precision
def test_datetime_to_json_with_custom_time_precision
with_standard_json_time_format(true) do
with_time_precision(0) do
- assert_equal "\"2000-01-01T00:00:00+00:00\"", ActiveSupport::JSON.encode(DateTime.new(2000))
+ assert_equal "\"2000-01-01T00:00:00+00:00\"", JSON.generate(DateTime.new(2000))
end
end
end
@@ -424,12 +424,12 @@ def test_datetime_to_json_with_custom_time_precision
def test_twz_to_json_when_wrapping_a_date_time
zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
time = ActiveSupport::TimeWithZone.new(DateTime.new(2000), zone)
- assert_equal '"1999-12-31T19:00:00.000-05:00"', ActiveSupport::JSON.encode(time)
+ assert_equal '"1999-12-31T19:00:00.000-05:00"', JSON.generate(time)
end
def test_exception_to_json
exception = Exception.new("foo")
- assert_equal '"foo"', ActiveSupport::JSON.encode(exception)
+ assert_equal '"foo"', JSON.generate(exception)
end
class InfiniteNumber
@@ -439,7 +439,7 @@ def as_json(options = nil)
end
def test_to_json_works_when_as_json_returns_infinite_number
- assert_equal '{"number":null}', InfiniteNumber.new.to_json
+ assert_equal '{"number":null}', JSON.generate(InfiniteNumber.new)
end
class NaNNumber
@@ -449,7 +449,7 @@ def as_json(options = nil)
end
def test_to_json_works_when_as_json_returns_NaN_number
- assert_equal '{"number":null}', NaNNumber.new.to_json
+ assert_equal '{"number":null}', JSON.generate(NaNNumber.new)
end
private
source 'https://rubygems.org'
gem 'activesupport', '5.1.0'
gem 'oj', '3.0.5'
gem 'benchmark-ips'

Time serializations

Time Type time_precision use_standard_json_time_format JSON.generate ActiveSupport::JSON.encode
time_with_zone 3 true "2017-01-01 09:00:00 +0900" "2017-01-01T09:00:00.123+09:00"
date_time 3 true "2017-01-01T00:00:00+00:00" "2017-01-01T00:00:00.123+00:00"
utc_time 3 true "2017-01-01 00:00:00 UTC" "2017-01-01T00:00:00.123Z"
local_time 3 true "2017-01-01 00:00:00 +0900" "2017-01-01T00:00:00.123+09:00"
date 3 true "2017-01-01" "2017-01-01"
time_with_zone 0 true "2017-01-01 09:00:00 +0900" "2017-01-01T09:00:00+09:00"
date_time 0 true "2017-01-01T00:00:00+00:00" "2017-01-01T00:00:00+00:00"
utc_time 0 true "2017-01-01 00:00:00 UTC" "2017-01-01T00:00:00Z"
local_time 0 true "2017-01-01 00:00:00 +0900" "2017-01-01T00:00:00+09:00"
date 0 true "2017-01-01" "2017-01-01"
time_with_zone 3 true "2017-01-01 09:00:00 +0900" "2017-01-01T09:00:00.123+09:00"
date_time 3 true "2017-01-01T00:00:00+00:00" "2017-01-01T00:00:00.123+00:00"
utc_time 3 true "2017-01-01 00:00:00 UTC" "2017-01-01T00:00:00.123Z"
local_time 3 true "2017-01-01 00:00:00 +0900" "2017-01-01T00:00:00.123+09:00"
date 3 true "2017-01-01" "2017-01-01"
time_with_zone 3 false "2017-01-01 09:00:00 +0900" "2017/01/01 09:00:00 +0900"
date_time 3 false "2017-01-01T00:00:00+00:00" "2017/01/01 00:00:00 +0000"
utc_time 3 false "2017-01-01 00:00:00 UTC" "2017/01/01 00:00:00 +0000"
local_time 3 false "2017-01-01 00:00:00 +0900" "2017/01/01 00:00:00 +0900"
date 3 false "2017-01-01" "2017/01/01"

Other serializations

Type options JSON.generate ActiveSupport::JSON.encode
standard_class "#StandardClass:0x007f8dc2a33e80" {"a":1}
hash_with_only {:only=>:a} {"a":1,"b":2} {"a":1}
hash_with_except {:except=>:a} {"a":1,"b":2} {"b":2}
with_to_hash "#WithToHash:0x007f8dc2a33cf0" {"a":1}
with_as_json "#WithAsJson:0x007f8dc2a33c00" "{:a=\u003e1}"
struct "#" {"a":1}
infinity - null
nan - null
enumerator "#Enumerator:0x007f8dc2a337c8" []
process_status "pid 5414 exit 0" {"exitstatus":0,"pid":5414}
special_chars "

><&" "\u2028\u2029\u003e\u003c\u0026"
require 'json'
require 'active_support/time'
require 'active_support/json'
Time.zone = 'Asia/Tokyo'
YEAR = 2017
USEC = 123456
TIME_ENTRIES = {
time_with_zone: Time.utc(YEAR).in_time_zone.change(usec: USEC),
date_time: DateTime.new(YEAR).change(usec: USEC),
utc_time: Time.utc(YEAR).change(usec: USEC),
local_time: Time.local(YEAR).change(usec: USEC),
date: Date.new(YEAR),
}
class StandardClass
def initialize(a)
@a = a
end
end
class WithToHash
def initialize(**kwargs)
@kwargs = kwargs
end
def to_hash
@kwargs
end
end
class WithAsJson
def initialize(**kwargs)
@kwargs = kwargs
end
def as_json(options = nil)
@kwargs.to_s
end
end
OTHER_ENTRIES = {
standard_class: [StandardClass.new(1)],
hash_with_only: [{ a: 1, b: 2 }, { only: :a }],
hash_with_except: [{ a: 1, b: 2 }, { except: :a }],
with_to_hash: [WithToHash.new(a: 1)],
with_as_json: [WithAsJson.new(a: 1)],
struct: [Struct.new(:a).new(1)],
infinity: [Float::INFINITY],
nan: [Float::NAN],
enumerator: [[].each],
process_status: [system('true') && $?],
special_chars: ["\u2028\u2029><&"],
}
def with_time_precision(value)
old_value = ActiveSupport::JSON::Encoding.time_precision
ActiveSupport::JSON::Encoding.time_precision = value
yield
ensure
ActiveSupport::JSON::Encoding.time_precision = old_value
end
def with_standard_json_time_format(value = true)
old_value = ActiveSupport.use_standard_json_time_format
ActiveSupport.use_standard_json_time_format = value
yield
ensure
ActiveSupport.use_standard_json_time_format = old_value
end
def build_time_results(time_entries)
time_entries.map do |label, time|
[
label,
ActiveSupport::JSON::Encoding.time_precision,
ActiveSupport.use_standard_json_time_format,
JSON.generate(time),
ActiveSupport::JSON.encode(time)
]
end
end
def show_time_encoding_results(time_entries)
results = []
[ActiveSupport::JSON::Encoding.time_precision, 0].each do |time_precision|
with_time_precision(time_precision) do
results.concat(build_time_results(time_entries))
end
end
[true, false].each do |use_standard_json_time_format|
with_standard_json_time_format(use_standard_json_time_format) do
results.concat(build_time_results(time_entries))
end
end
puts '## Time serializations'
puts ''
puts ' Time Type | time_precision | use_standard_json_time_format | JSON.generate | ActiveSupport::JSON.encode '
puts (['--------'] * results[0].size).join('|')
results.each do |result|
puts result.join(' | ')
end
end
def show_entry_encoding_results(entries)
puts '## Other serializations'
puts ''
puts ' Type | options | JSON.generate | ActiveSupport::JSON.encode '
puts (['--------'] * 4).join('|')
entries.each do |label, (entry, opts)|
puts [
label,
opts,
begin; JSON.generate(entry, opts); rescue; '-' end,
ActiveSupport::JSON.encode(entry, opts)
].join(' | ')
end
end
show_time_encoding_results(TIME_ENTRIES)
puts ''
show_entry_encoding_results(OTHER_ENTRIES)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment