Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

360 lines (253 sloc) 12.365 kB

Jsonify — a builder for JSON Build Status

Jsonify is to JSON as Builder is to XML.

Goal

Jsonify provides a builder style engine for creating correct JSON representations of Ruby objects.

Jsonify hooks into Rails ActionView to allow you to create JSON view templates in much the same way that you can use Builder for XML templates.

Motivation

JSON and XML are without a doubt the most common representations used by RESTful applications. Jsonify was built around the notion that these representations belong in the view layer of the application. For XML representations, Rails makes this easy through its support of Builder templates, but, when it comes to JSON, there is no clear approach.

For many applications, particularly those based on legacy database, its not uncommon to expose the data in more client-friendly representations than would be presented by the default Rails to_json method. Rails does provide control of the emitted via a custom implementation of as_json. Nevertheless, this forces the developer to place this code into the model when it more rightly belongs in the view.

When someone asks "Where are the model representations defined?", I don't want to have to say "Well, look in the views folder for XML, but you have to look at the code in the model for the JSON format."

There are a number of other libraries available that try to solve this problem. Some take a similar approach to Jsonify and provide a builder-style interface. Others allow the developer to specify the representation using a common DSL that can generate both JSON and XML. Please take a look at these projects when you consider alternatives. It's my opinion that there are substantial and inherent differences between XML and JSON; and that these differences may force the developer to make concessions in one format or the other.

But an even greater motivation for me was emulating the simplicity of Builder. I have not found a single framework for JSON that provides the simplicity and elegance of Builder. Jsonify is my attempt at remedying that situation.

Installation

gem install jsonify

Usage

In the examples that follow, the JSON output is usually shown "prettified". Is this only for illustration purposes, as the default behavior for Jsonify is not to prettify the output. You can enable prettification by passing :pretty => true to the Jsonify::Builder constructor; however, pretty printing is a relatively costly operation and should not be used in production (unless, of course, you explicitly want to show this format).

Standalone

# Create some objects that represent a person and associated hyperlinks
@person = Struct.new(:first_name,:last_name).new('George','Burdell')
@links = [
  ['self',   'http://example.com/people/123'],
  ['school', 'http://gatech.edu'],
]

# Build this information as JSON
require 'jsonify'
json = Jsonify::Builder.new(:pretty => true)

json.result do
  json.alumnus do
    json.fname @person.first_name
    json.lname @person.last_name
  end
  json.links(@links) do |link|
    {:rel => link.first, :href => link.last}
  end
end

# Evaluate the result to a string
json.compile!

Results in ...

{
  "result": {
    "alumnus": {
      "fname": "George",
      "lname": "Burdell"
    },
    "links": [
      {
        "rel": "self",
        "href": "http://example.com/people/123"
      },
      {
        "rel": "school",
        "href": "http://gatech.edu"
      }
    ]
  }
}

View Templates

Jsonify includes Rails 3 template handler. Rails will handle any template with a .jsonify extension with Jsonify. The Jsonify template handler exposes the Jsonify::Builder instance to your template with the json variable as in the following example:

json.hello do
  json.world "Jsonify is Working!"
end

Partials

You can use partials with Jsonify view templates, but how you use them will depend on how the information they return is structured. An important to keep in mind is that a partial, no matter what kind it is, always a returns a string.

Jsonify partials

Any Jsonify partial — that is, the file has a .jsonify extension — will return, by design, a string that is valid JSON. It will represent either a JSON object, and be wrapped in curly braces ({}), or a JSON array, and be wrapped in square brackets ([]).

To incorporate such a value into a Jsonify template, Jsonify provides the ingest! method.

You can use this method to add JSON, rendered by a partial, to the builder. Let's assume this our main template, index.jsonify:

json << 1
json.ingest! (render :partial=>'my_partial')

From the first line, you can tell that an array will be created so this line uses the append operator. On the second line, a partial is being added to the builder. Note that you cannot simply place render :parial ... on a line by itself as you can do with other templates like erb and haml; you have to explicitly tell Jsonify to add it.

Let's say that the partial file, _my_partial.jsonify, is as follows:

json << 3
json << 4

This json variable is a separate instance of the Jsonify::Builder, distinct from the builder instance, in the main template. This partial will end up generating the following string:

"[3,4]"

The ingest! method will actually parse this string back into a Jsonify-based object, and add it to the builder's current state. The resulting output will be:

"[1,[3,4]]"
Other partials

You can also use output from non-Jsonify templates (e.g. erb); just remember that the output from a template is always a string and that you have to tell the builder how to include the result of the partial. For example, suppose I have the partial _today.erb with the following content:

<%= Date.today %>

You can then incorporate this partial into your Jsonify template just as you would any other string value:

json << 1
json.ingest! (render :partial=>'my_partial')
json << {:date => (render :partial => 'today')}

renders ...

[1,[3,4],{"date":"2011-07-30"}]

Usage Patterns

Jsonify is designed to support construction of an valid JSON representation and is entirely based on the JSON specification.

JSON is built on two fundamental structures:

  • object: a collection of name-value pairs -- in Jsonify this is a JsonObject
  • array: an ordered list of values -- in Jsonify this is a JsonArray

Jsonify adheres to the JSON specification and provides explicit support for working with these primary structures. At the top most level, a JSON string must be one of these structures and Jsonify ensures that this condition is met.

JSON Objects

A JSON object, sometimes referred to as an object literal, is a common structure familiar to most developers. Its analogous to the nested element structured common in XML. The JSON RFC states that "the names within an object SHOULD be unique". Jsonify elevates this recommendation by backing the JsonObject with a Hash; an object must have unique keys and the last one in, wins.

json = Jsonify::Builder.new
json.person do # start a new JsonObject where the key is 'foo'
  json.name 'George Burdell' # add a pair to this object
  json.skills ['engineering','bombing'] # adds a pair with an array value
  json.name 'George P. Burdell'
end

compiles to ...

{
  "person": {
    "name": "George P. Burdell",
    "skills": [
      "engineering",
      "bombing"
    ]
  }
}

It's perfectly legitimate for a JSON representation to simply be a collection of name-value pairs without a root element. Jsonify supports this by simply allowing you to specify the pairs that make up the object.

json = Jsonify::Builder.new
json.location 'Library Coffeehouse'
json.neighborhood 'Brookhaven'

compiles to ...

{
  "location": "Library Coffeehouse",
  "neighborhood": "Brookhaven"
}

If the name you want contains whitespace or other characters not allowed in a Ruby method name, use tag!.

json.tag!("my location", 'Library Coffeehouse')
json.neighborhood 'Brookhaven'

{
  "my location": "Library Coffeehouse",
  "neighborhood": "Brookhaven"
}

Jsonify also supports a hash-style interface for creating JSON objects.

json = Jsonify::Builder.new

json[:foo] = :bar
json[:go]  = :far

compiles to ...

{
  "foo": "bar",
  "go": "far"
}

You can these hash-style methods within a block as well ...

json.homer do
  json[:beer] = "Duffs"
  json[:spouse] = "Marge"
end

compiles to ...

{
  "homer": {
    "beer": "Duffs",
    "spouse": "Marge"
  }
}

If you prefer a more method-based approach, you can use the store! method passing it the key and value.

json.store!(:foo, :bar)
json.store!(:go, :far)

JSON Arrays

A JSON array is an ordered list of JSON values. A JSON value can be a simple value, like a string or a number, or a supported JavaScript primitive like true, false, or null. A JSON value can also be a JSON object or another JSON array. Jsonify strives to make this kind of construction possible in a buider-style.

Jsonify supports JSON array construction through two approaches: method_missing and append!.

method_missing

Pass an array and a block to method_missing (or tag!), and Jsonify will iterate over that array, and create a JSON array where each array item is the result of the block. If you pass an array that has a length of 5, you will end up with a JSON array that has 5 items. That JSON array is then set as the value of the name-value pair, where the name comes from the method name (for method_missing) or symbol (for tag!).

So this construct is really doing two things -- creating a JSON pair, and creating a JSON array as the value of the pair.

json = Jsonify::Builder.new(:pretty => true)
json.letters('a'..'c') do |letter|
  letter.upcase
end

compiles to ...

{
  "letters": [
    "A",
    "B",
    "C"
  ]
}

Another way to handle this particular example is to get rid of the block entirely. Simply pass the array directly — the result will be the same.

json.letters ('a'..'c').map(&:upcase)
append!

But what if we don't want to start with an object? How do we tell Jsonify to start with an array instead?

You can use append! (passing one or more values), or << (which accepts only a single value) to the builder and it will assume you are adding values to a JSON array.

json = Jsonify::Builder.new
json.append! 'a'.upcase, 'b'.upcase, 'c'.upcase

[
  "A",
  "B",
  "C"
]

or more idiomatically ...

json.append! *('a'..'c').map(&:upcase)

The append operator, <<, can be used to push a single value into the array:

json << 'a'.upcase
json << 'b'.upcase
json << 'c'.upcase

Of course, standard iteration works here as well ...

json = Jsonify::Builder.new
('a'..'c').each do |letter|
  json << letter.upcase
end

Mixing JSON Arrays and Objects

coming soon

Documentation

Yard Docs

Related Projects

TODOs

  1. Benchmark performance
  2. Document how partials can be used
  3. Clean up specs

Roadmap

  1. Split Rails template handling into separate gem
  2. Add support for Sinatra and Padrino (Tilt integration?)

License

This project is released under the MIT license.

Authors

Jump to Line
Something went wrong with that request. Please try again.