From 0f33d70e89991711ff8b3dde134a61f4a5a0ec06 Mon Sep 17 00:00:00 2001 From: Godfrey Chan Date: Tue, 12 Nov 2013 01:57:21 -0800 Subject: [PATCH] Improved compatibility with the stdlib JSON gem. Previously, calling `::JSON.{generate,dump}` sometimes causes unexpected failures such as intridea/multi_json#86. `::JSON.{generate,dump}` now bypasses the ActiveSupport JSON encoder completely and yields the same result with or without ActiveSupport. This means that it will **not** call `as_json` and will ignore any options that the JSON gem does not natively understand. To invoke ActiveSupport's JSON encoder instead, use `obj.to_json(options)` or `ActiveSupport::JSON.encode(obj, options)`. --- activesupport/CHANGELOG.md | 14 ++++++ .../active_support/core_ext/object/json.rb | 24 ++++++++-- activesupport/test/json/encoding_test.rb | 45 +++++++++++++++++++ 3 files changed, 80 insertions(+), 3 deletions(-) diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index ee05ea3255..bb66b0ffa2 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,17 @@ +* Improved compatibility with the stdlib JSON gem. + + Previously, calling `::JSON.{generate,dump}` sometimes causes unexpected + failures such as intridea/multi_json#86. + + `::JSON.{generate,dump}` now bypasses the ActiveSupport JSON encoder + completely and yields the same result with or without ActiveSupport. This + means that it will **not** call `as_json` and will ignore any options that + the JSON gem does not natively understand. To invoke ActiveSupport's JSON + encoder instead, use `obj.to_json(options)` or + `ActiveSupport::JSON.encode(obj, options)`. + + *Godfrey Chan* + * Fix Active Support `Time#to_json` and `DateTime#to_json` to return 3 decimal places worth of fractional seconds, similar to `TimeWithZone`. diff --git a/activesupport/lib/active_support/core_ext/object/json.rb b/activesupport/lib/active_support/core_ext/object/json.rb index 898c3f4307..5157b0402f 100644 --- a/activesupport/lib/active_support/core_ext/object/json.rb +++ b/activesupport/lib/active_support/core_ext/object/json.rb @@ -9,18 +9,36 @@ require 'time' require 'active_support/core_ext/time/conversions' require 'active_support/core_ext/date_time/conversions' require 'active_support/core_ext/date/conversions' +require 'active_support/core_ext/module/aliasing' # The JSON gem adds a few modules to Ruby core classes containing :to_json definition, overwriting # their default behavior. That said, we need to define the basic to_json method in all of them, # otherwise they will always use to_json gem implementation, which is backwards incompatible in # several cases (for instance, the JSON implementation for Hash does not work) with inheritance # and consequently classes as ActiveSupport::OrderedHash cannot be serialized to json. +# +# On the other hand, we should avoid conflict with ::JSON.{generate,dump}(obj). Unfortunately, the +# JSON gem's encoder relies on its own to_json implementation to encode objects. Since it always +# passes a ::JSON::State object as the only argument to to_json, we can detect that and forward the +# calls to the original to_json method. +# +# It should be noted that when using ::JSON.{generate,dump} directly, ActiveSupport's encoder is +# bypassed completely. This means that as_json won't be invoked and the JSON gem will simply +# ignore any options it does not natively understand. This also means that ::JSON.{generate,dump} +# should give exactly the same results with or without active support. [Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass].each do |klass| klass.class_eval do - # Dumps object in JSON (JavaScript Object Notation). See www.json.org for more info. - def to_json(options = nil) - ActiveSupport::JSON.encode(self, options) + def to_json_with_active_support_encoder(options = nil) + if options.is_a?(::JSON::State) + # Called from JSON.{generate,dump}, forward it to JSON gem's to_json + self.to_json_without_active_support_encoder(options) + else + # to_json is being invoked directly, use ActiveSupport's encoder + ActiveSupport::JSON.encode(self, options) + end end + + alias_method_chain :to_json, :active_support_encoder end end diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb index 856ca75cbc..39da760bf2 100644 --- a/activesupport/test/json/encoding_test.rb +++ b/activesupport/test/json/encoding_test.rb @@ -32,6 +32,19 @@ class TestJSONEncoding < ActiveSupport::TestCase end end + class HashWithAsJson < Hash + attr_accessor :as_json_called + + def initialize(*) + super + end + + def as_json(options={}) + @as_json_called = true + super + end + end + TrueTests = [[ true, %(true) ]] FalseTests = [[ false, %(false) ]] NilTests = [[ nil, %(null) ]] @@ -367,6 +380,38 @@ class TestJSONEncoding < ActiveSupport::TestCase assert_equal false, false.as_json end + def test_json_gem_dump_by_passing_active_support_encoder + h = HashWithAsJson.new + h[:foo] = "hello" + h[:bar] = "world" + + assert_equal %({"foo":"hello","bar":"world"}), JSON.dump(h) + assert_nil h.as_json_called + end + + def test_json_gem_generate_by_passing_active_support_encoder + h = HashWithAsJson.new + h[:foo] = "hello" + h[:bar] = "world" + + assert_equal %({"foo":"hello","bar":"world"}), JSON.generate(h) + assert_nil h.as_json_called + end + + def test_json_gem_pretty_generate_by_passing_active_support_encoder + h = HashWithAsJson.new + h[:foo] = "hello" + h[:bar] = "world" + + assert_equal <