#322 RABL
- Download:
- source codeProject Files in Zip (100 KB)
- mp4Full Size H.264 Video (19.6 MB)
- m4vSmaller H.264 Video (10.2 MB)
- webmFull Size VP8 Video (12.2 MB)
- ogvFull Size Theora Video (23.6 MB)
If we want to return custom JSON data from a Rails application based on model data we can do this in a number of ways. We could override the as_json
method in the model or use the Jbuilder gem as we did in episode 320. Another popular solution is the RABL gem. RABL stands for Ruby API Builder Language and is a little more feature-complete that some of the other solutions.
We’ll show you how RABL works in this episode, using the same example app we used in the Jbuilder episode to give you an idea of the differences between the two libraries. This app is a blogging application that has a number of articles and we want to able to view a single article’s data as JSON by appending .json
to the article’s URL. If we try this now we’ll just get a an error message as the application doesn’t know how to respond to JSON requests.
Getting Started With RABL
To do this we could modify the ArticlesController
, make sure that the show action responds to JSON requests and then return a JSON representation of the selected article, but instead we’re going to use RABL. This comes as a gem and is installed in the usual way, by adding it to the gemfile and running bundle
.
gem 'rabl'
Like Jbuilder, RABL includes a template handler so that we can define the JSON response in the view layer. We’ll create one of these now for creating a JSON representation of an article. We can use Ruby code to do this using RABL’s DSL.
object @article attributes :id, :name, :published_at
The first thing in a RABL template is usually a call to object
and we pass this the object we want to work with. We can then call attributes
to define the attributes of the object that we want to return. If we visit the article’s JSON URL now we’ll see the JSON we defined in the template returned.
$ curl http://localhost:3000/articles/1.json {"article":{"id":1,"name":"Superman","published_at":"2012-01-19T18:38:50Z"}}
If we want to specify something that isn’t a simple attribute we can use the node
method and pass it a name and a block. Whatever the block returns will be used as the value.
object @article attributes :id, :name, :published_at node(:edit_url) { "..." }
This will add an edit_url attribute to the JSON.
$ curl http://localhost:3000/articles/1.json {"article":{"id":1,"name":"Superman","published_at":"2012-01-19T18:38:50Z","edit_url":"..."}}
We have access to helper methods in RABL templates so we can use edit_article_url
to get the article’s edit URL and current_user
so that the URL is only shown if the current user is an admin (assuming that the authentication solution we’re using has a current_user
helper method).
object @article attributes :id, :name, :published_at if current_user.admin? node(:edit_url) { edit_article_url(@article) } end
There’s an issue with this, though. We’re using the article instance variable in edit_article_url
which isn’t a good practice. We should instead use the article object that’s passed to the block.
object @article attributes :id, :name, :published_at if current_user.admin? node(:edit_url) { |article| edit_article_url(article) } end
The object that is passed to the block is the same object that’s passed to the object
method but there’s a good reason why we should use the object passed to the block instead and we’ll explain why later in this episode. If we look at the article’s JSON now we’ll see the edit_url
attribute with the correct value.
$ curl http://localhost:3000/articles/1.json {"article":{"id":1,"name":"Superman","published_at":"2012-01-19T18:38:50Z","edit_url":"http://localhost:3000/articles/1/edit"}}
Associated Records
If we want to include data from associated records it’s each to do so. In our application an Article
belongs to an Author
and has many Comments
. We can use RABL’s child
method to include information from these associated models.
object @article attributes :id, :name, :published_at if current_user.admin? node(:edit_url) { |article| edit_article_url(article) } end child :author do attributes :id, :name node(:url) { |author| author_url(author) } end
The code to generate JSON for the associated author is very similar to the code for generating the article’s JSON. The only difference is that an author is passed to node’s block instead of an article. When we view the JSON now we’ll see the article’s author’s details just like we expect.
$ curl http://localhost:3000/articles/1.json {"article":{"id":1,"name":"Superman","published_at":"2012-01-19T18:38:50Z","edit_url":"http://localhost:3000/articles/1/edit","author":{"id":2,"name":"Clark Kent","url":"http://localhost:3000/authors/2"}}}
The same can be done for the Comments
association, though as we’re dealing with a has_many
association we use the plural name.
object @article attributes :id, :name, :published_at if current_user.admin? node(:edit_url) { |article| edit_article_url(article) } end child :author do attributes :id, :name node(:url) { |author| author_url(author) } end child :comments do attributes :id, :name, :content end
In the JSON response the comments will be nested in an array, not just displayed in a single record as the author was.
$ curl http://localhost:3000/articles/1.json {"article":{"id":1,"name":"Superman","published_at":"2012-01-19T18:38:50Z","edit_url":"http://localhost:3000/articles/1/edit","author":{"id":2,"name":"Clark Kent","url":"http://localhost:3000/authors/2"},"comments":[{"comment":{"id":1,"name":"Lois Lane","content":"Does anyone know where I can find Superman?"}},{"comment":{"id":2,"name":"Lex Luthor","content":"I have some Kryptonite for you Superman!"}}]}}
Reusing Templates
Our JSON data is nicely defined for an article but what if we want to use this output elsewhere in our application? We might want the
collection @articles extends "articles/show"
As we’re working with multiple articles we use collection
instead of object
here. We could define the attributes we want to use here like we did in the show template but as we want the same JSON for each article that we can reuse that template here by calling extends
and passing in its name. If we look at articles.json
now we’ll see the JSON data for all the articles.
$ curl http://localhost:3000/articles.json [{"article":{"id":1,"name":"Superman","published_at":"2012-01-19T18:38:50Z","edit_url":"http://localhost:3000/articles/1/edit","author":{"id":2,"name":"Clark Kent","url":"http://localhost:3000/authors/2"},"comments":[{"comment":{"id":1,"name":"Lois Lane","content":"Does anyone know where I can find Superman?"}},{"comment":{"id":2,"name":"Lex Luthor","content":"I have some Kryptonite for you Superman!"}}]}},{"article":{"id":2,"name":"Krypton","published_at":"2012-01-05T18:38:50Z","edit_url":"http://localhost:3000/articles/2/edit","author":{"id":2,"name":"Clark Kent","url":"http://localhost:3000/authors/2"},"":[]}},{"article":{"id":3,"name":"Batman & Robin","published_at":"2012-01-26T18:38:50Z","edit_url":"http://localhost:3000/articles/3/edit","author":{"id":1,"name":"Bruce Wayne","url":"http://localhost:3000/authors/1"},"comments":[{"comment":{"id":3,"name":"The Joker","content":"Haha, Batman, you will see your bat signal tonight!"}},{"comment":{"id":4,"name":"Robin","content":"Enough with the games Joker."}},{"comment":{"id":5,"name":"Riddler","content":"Did someone say games?"}}]}}]
The ability to reuse templates like this is the reason why we should minimize the use of instance variables in RABL templates. As we only use the @article
variable in the show template when we define the object it’s easier to reuse this template in the index template as we’re using the article that’s passed into the block in the call to node instead of the instance variable.
Root Nodes
One thing you might notice about the JSON that RABL outputs is that it includes the model name as the root node.
$ curl http://localhost:3000/articles/1.json {"article":{"id":1,"name":"Superman","published_at":"2012-01-19T18:38:50Z","edit_url":"http://localhost:3000/articles/1/edit","author":{"id":2,"name":"Clark Kent","url":"http://localhost:3000/authors/2"},"comments":[{"comment":{"id":1,"name":"Lois Lane","content":"Does anyone know where I can find Superman?"}},{"comment":{"id":2,"name":"Lex Luthor","content":"I have some Kryptonite for you Superman!"}}]}}
Sometimes we want this but sometimes we don’t. In Rails 3.1 the default JSON output didn’t include the root element and if we want RABL to match this we’ll need to configure it. We can do this in a new file in the /config/initializers
directory, setting include_json_root
to false.
Rabl.configure do |config| config.include_json_root = false end
As we’ve created an initializer we’ll need to restart the app for these changes to be picked up. After we have the first article’s JSON will no longer include that root node.
$ curl http://localhost:3000/articles/1.json {"id":1,"name":"Superman","published_at":"2012-01-19T18:38:50Z","edit_url":"http://localhost:3000/articles/1/edit","author":{"id":2,"name":"Clark Kent","url":"http://localhost:3000/authors/2"},"comments":[{"id":1,"name":"Lois Lane","content":"Does anyone know where I can find Superman?"},{"id":2,"name":"Lex Luthor","content":"I have some Kryptonite for you Superman!"}]}
There are details of the configuration options that can be set in the Configuration section of RABL’s README file. There are other features that we haven’t covered here. Other serialization options are covered such as XML and Message Pack.
Embedding JSON in a Web Page
Sometimes we need to embed JSON data in an HTML document instead of calling a separate controller action. How do we do this with something like RABL? The HTML template for the index action looks like this:
<h1>Articles</h1> <div id="articles"> <% @articles.each do |article| %> <h2> <%= link_to article.name, article %> <span class="comments">(<%= pluralize(article.comments.size, 'comment') %>)</span> </h2> <div class="info"> by <%= article.author.name %> on <%= article.published_at.strftime('%b %d, %Y') %> </div> <div class="content"><%= article.content %></div> <% end %> </div>
We’ll add the JSON for the articles in a data-
attribute in the wrapper div
. We could just call @articles.to_json
but this won’t use the RABL template. Instead we can call render(:template)
and pass the template’s name.
<div id="articles" data-articles="<%= render(template: "articles/index.json.rabl") %>" >
If we reload the page and view the source now we’ll see that embedded JSON.
<div id="articles" data-articles="[{"id":1,"name":"Superman","published_at":"2012-01-19T18:38:50Z","edit_url":"http://localhost:3000/articles/1/edit","author":{"id":2,"name":"Clark Kent","url":"http://localhost:3000/authors/2"},"comments":[{"id":1,"name":"Lois Lane","content":"Does anyone know where I can find Superman?"},{"id":2,"name":"Lex Luthor","content":"I have some Kryptonite for you Superman!"}]},{"id":2,"name":"Krypton","published_at":"2012-01-05T18:38:50Z","edit_url":"http://localhost:3000/articles/2/edit","author":{"id":2,"name":"Clark Kent","url":"http://localhost:3000/authors/2"},"":[]},{"id":3,"name":"Batman & Robin","published_at":"2012-01-26T18:38:50Z","edit_url":"http://localhost:3000/articles/3/edit","author":{"id":1,"name":"Bruce Wayne","url":"http://localhost:3000/authors/1"},"comments":[{"id":3,"name":"The Joker","content":"Haha, Batman, you will see your bat signal tonight!"},{"id":4,"name":"Robin","content":"Enough with the games Joker."},{"id":5,"name":"Riddler","content":"Did someone say games?"}]}]" >
If we do use this technique it’s important that any instance variables use inside the rendered template are set in the controller’s action.