Understanding Ruby and Rails: Serializing Ruby objects with JSON

Note

This article targets Rails 2.3 and Rails 3. The information contained in this page might not apply to different versions.

This article is part of my series Understanding Ruby and Rails. Please see the table of contents for the series to view the list of all posts.

A few weeks ago, Alan Skorkin posted a nice article about serializing objects with Ruby introducing different serialization options, including YAML, Marshaling and JSON. This reminded me of an article I always wanted to write about ActiveSupport::JSON module, as part of the inside Ruby on Rails series.

What is JSON?

JSON (JavaScript Object Notation) is a lightweight data-interchange format. More importantly, JSON is a human-readable serialization format, like the popular YAML format all Rubyists are probably familiar with.

Compared to other serialization alternatives such as XML, YAML or binary serialization, JSON offers the following advantages:

  • it's a human-readable format
  • it's largely adopted and supported by most programming languages
  • it's a language-independent format
  • it can be compressed in one line to reduce stream size
  • it can represent most standard objects
  • it seamlessly integrates with JavaScript which makes JSON the standard for streaming data over AJAX calls

All these features make JSON an excellent serialization format. Of course, there are also some drawbacks, but this is material for another article.

JSON and Ruby on Rails

Ruby on Rails is a web application framework and JSON is strictly related to the web ecosystem as a subset of the JavaScript programming language. There are many different parts of a Ruby on Rails application where you might need to manipulate, encode and decode a JSON string into a Ruby object and vice-versa.

JSON support in Ruby on Rails is provided by the ActiveSupport::JSON module. Behind the scenes, ActiveSupport wraps the JSON library, a standard Ruby gem which you can use in any Ruby project. However, ActiveSupport goes beyond the boundary of a simple wrapper: it provides a JSON definition for most Ruby objects making JSON an effective full drop-in replacement for YAML. I'm going to talk about this later in this article.

ActiveSupport::JSON

As I mentioned before, ActiveSupport::JSON relies on the JSON gem thus you need to have both libraries installed on your system. If you installed the Ruby on Rails framework, then you already have everything you need to start working with JSON.

The module provides a super-simple API composed of two methods:

  • ActiveSupport::JSON.encode(object): takes a Ruby object as value and returns a JSON-encoded string.
  • ActiveSupport::JSON.decode(string): takes a JSON-encoded string and returns the corresponding Ruby object

Here are a few examples:

j = ActiveSupport::JSON
ruby-1.8.7-p249 > j.encode(23)
# => "23"
j.encode("A string")
# => "A string"
j.encode({ :color => ["red", "green", "yellow"] })
# => {"color":["red","green","yellow"]}
j.encode({ :color => ["red", "green", "yellow"], :date => Time.now })
# => {"color":["red","green","yellow"],"date":"2010-04-29T00:28:56+02:00"}

j.decode(j.encode({ :color => ["red", "green", "yellow"], :date => Time.now }))
# => {"date"=>"2010-04-29T00:25:52+02:00", "color"=>["red", "green", "yellow"]}

As you can see, the usage is really straightforward and the JSON-encoded result is smaller than its YAML and XML counterparts.

v = { :color => ["red", "green", "yellow"], :date => Time.now }
# => {:color=>["red", "green", "yellow"], :date=>Thu Apr 29 00:28:56 +0200 2010} 

ActiveSupport::JSON.encode(v)
# 69 bytes
# => {"color":["red","green","yellow"],"date":"2010-04-29T00:28:56+02:00"}

YAML.dump(v)
# ---
# :color:
# - red
# - green
# - yellow
# :date: 2016-08-06 13:08:09.592621000 +02:00

ActiveSupport::JSON vs JSON

At the beginning of the article, I said ActiveSupport::JSON is something more than a mere JSON wrapper. Now it's time to explain that statement.

The JSON format natively supports only a limited subset of variable types such as String, Number, Array and Hash. Understandably, being a language-agnostic format, it doesn't support complex or Ruby-specific objects such as Object, Exception or Range. For this reason, the JSON library delegates to each class the implementation of the JSON representation of the object using the to_json method. Similar to other standard transformation methods such as to_s or to_f, to_json is supposed to return a JSON-compatible representation.

While the JSON library now ships with a number of prepackaged definitions, by default it doesn't support most of the standard Ruby objects. Also, it doesn't support the serialization of ActiveRecord objects and, working with Rails projects and database records, this might be a huge limitation.

ActiveSupport::JSON solves this problem and provides a predefined to_json implementation for most Ruby/Rails objects. It also defines a simple Object#to_json making virtually every Ruby object JSON-compatible.

As of ActiveSupport 2.3.5, the following classes are supported: String, Symbol, Date, Time, DateTime, Enumerable, Array, Hash, FalseClass, TrueClass, NilClass, Numeric, Float, Integer, Regexp, and Object. You can find them in the lib/active_support/json/encoders folder. The ActiveRecord serialization/deserialization strategy is defined in the ActiveRecord library in lib/active_record/serializers/json_serializer.rb:

def to_json(options = {})
  super
end

def as_json(options = nil) #:nodoc:
  hash = Serializer.new(self, options).serializable_record
  hash = { self.class.model_name.element => hash } if include_root_in_json
  hash
end

def from_json(json)
  self.attributes = ActiveSupport::JSON.decode(json)
  self
end

Compared with the JSON gem, ActiveSupport::JSON is the answer to Alan's statement:

There is bad news of course, in that your objects won't automagically be converted to JSON, unless all you're using is hashes, arrays and primitives. You need to do a little bit of work to make sure your custom object is serializable. Let's make one of the classes we introduced previously serializable using JSON.

As a side note, it also provides some additional features such as an interchangeable encoding/decoding backend.

One final note about Rails 3 (and ActiveSupport 3): the ActiveSupport::JSON module has been recently modified in Rails 3. Objects without a native JSON representation are now encoded using to_hash or to_s instead of instance_values.