Understanding Ruby and Rails: Serializing Ruby objects with JSON

This article targets Rails 2.3 Rails 3

The information contained in this page might not apply to different versions.

This is 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 important, JSON is a human readable serialization format, like the popular YAML format all Rubyist 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 the most part of programming languages
  • it's a language-independent format
  • can be compressed in one line to reduce stream size
  • can represent the most part of standard objects
  • 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 an other 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 the most part of the 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 by two methods:

  • ActiveSupport::JSON.encode(object)
  • ActiveSupport::JSON.decode(string)

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

Here's 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", "jellow"] })
# => {"color":["red","green","jellow"]}
j.encode({ :color => ["red", "green", "jellow"], :date => Time.now })
# => {"color":["red","green","jellow"],"date":"2010-04-29T00:28:56+02:00"}

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

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

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

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

YAML.dump(v)
# 84 bytes
# ---
# :color:
# - "red"
# - "green"
# - "jellow"
# :date: 2010-04-29 00:28:56.827076 +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 the time to explain that statement.

The JSON format natively supports only a limited subset of variable types such as String, Number, Array and Hash. Easy to understand, being a language-agnostic format, it doesn't support complex or ruby-specific objects such as Object, Exception or Range. For this reason, 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 the most part of 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 JSON Gem, ActiveSupport::JSON is the solution to the following 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.