Boxer: Generating Sane JSON from Complex Objects for Our API
Written by Brad FultsThe Problem
Here at Gowalla we have a large API with many different types of objects distributed across many different resources. So composing and showing views of those objects for API responses has been a challenge.
We started out by just rendering ActiveRecord models directly to JSON:
class Api::UsersController < ApiController
def show
@user = User.find(params[:id])
render :json => @user
end
end
But that quickly became unwieldy as we wanted full and simple control over
which attributes are returned, and to start returning method values alongside
model attributes. So we moved on to writing .json.erb files, which allowed
us to specify exactly what appears in a response:
# app/views/users/show.json.erb
<%= raw({
:name => @user.name,
:recent_spots => @user.recent_spots(30),
:is_private => @user.private?
}.to_json) %>
Though after a long while, these views ended up growing out of control, with conditionals based on the authenticated user requesting the information, whether they're friends, whether the user resource is a business, whether a promotion is running, etc.
# app/views/users/show.json.erb
<%
hash = {
:name => @user.name,
:is_private => @user.private?
}
if user.public?
hash.merge!({
:recent_spots => @user.recent_spots(30),
:recent_highlights => @user.recent_highlights(10),
})
end
if user.friends?(current_user)
hash.merge!({
:is_friend => true,
:mutual_friends => user.mutual_friends(current_user).map(&:to_hash),
})
end
%>
<%= raw(hash.to_json) %>
The Solution
So during our developing of Gowalla 4, we came across Nathan Esquenazi's RABL library, which is a great attempt at attacking this problem. For our purposes, though, we went with a simpler and less robust framework that would do exactly what we wanted.
Our simple framework, Boxer, knows about different object representations, the various views each of our objects can have and will let us compose objects together in responses that satisfy the complexity of our large API.
Instead of defining messy conditional views full of merged hashes, Boxer provides a clean framework for describing the representation and views of an object as hashes:
Boxer.box(:user) do |box, user|
box.view(:base) do
{
:name => user.name,
:age => user.age,
}
end
box.view(:full, :extends => :base) do
{
:email => user.email,
:is_private => user.private?,
}
end
end
Then you can "ship" the contents of a box with a model object (or any other arguments you want to pass in) and get a JSON-ready hash:
>> Boxer.ship(:user, User.first, :view => :full).to_json
=> "{"name": "Bo Jim", "age": 17, "email": "b@a.com", "is_private": false}"
In our case, our controllers do more or less a straight render of a shipped box:
def show
@object = Object.find(params[:id])
render :json => Boxer.ship(:object, @object, :view => :full)
end
For more examples and a more in-depth explanation, see the Boxer README and the project wiki.
Boxer also goes through the trouble of solving small and common problems that creep up when using this style of framework. Things like preconditions, helper methods, including methods for use in boxes and multiple box inheritance.
We have released Boxer as a gem and, as noted, an open-source project on GitHub. Let us know what interesting uses you find for Boxer, in addition to any suggested improvements.