[Ruby on Rails (RoR)] Use rails jbuilder to generate JSON objects with a Builder-style DSL

jbuilder

Jbuilder gives you a simple DSL for declaring JSON structures that beats manipulating giant hash structures. This is particularly helpful when the generation process is fraught with conditionals and loops. JSON objects can be use in RESTful API or composing objects.

Example

Here’s a simple example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# app/views/messages/show.json.jbuilder

json.content format_content(@message.content)
# You can use the call syntax instead of an explicit extract! call:
json.extract! @message, :created_at, :updated_at
json.(@message, :created_at, :updated_at)

json.author do
json.name @message.creator.name.familiar
json.email_address @message.creator.email_address_with_name
json.url url_for(@message.creator, format: :json)
end

if current_user.admin?
json.visitors calculate_visitors(@message)
end

json.comments @message.comments, :content, :created_at

json.attachments @message.attachments do |attachment|
json.filename attachment.filename
json.url url_for(attachment)
end

This will build the following structure:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"content": "<p>This is <i>serious</i> monkey business</p>",
"created_at": "2011-10-29T20:45:28-05:00",
"updated_at": "2011-10-29T20:45:28-05:00",

"author": {
"name": "David H.",
"email_address": "'David Heinemeier Hansson' <[email protected]>",
"url": "http://example.com/users/1-david.json"
},

"visitors": 15,

"comments": [
{ "content": "Hello everyone!", "created_at": "2011-10-29T20:45:28-05:00" },
{ "content": "To you my good sir!", "created_at": "2011-10-29T20:47:28-05:00" }
],

"attachments": [
{ "filename": "forecast.xls", "url": "http://example.com/downloads/forecast.xls" },
{ "filename": "presentation.pdf", "url": "http://example.com/downloads/presentation.pdf" }
]
}

Dynamical Attribute

To define attribute and structure names dynamically, use the set! method:

1
2
3
4
5
json.set! :author do
json.set! :name, 'David'
end

# => {"author": { "name": "David" }}

Attribute Merge

To merge existing hash or array to current context:

1
2
3
4
5
6
7
hash = { author: { name: "David" } }
json.post do
json.title "Merge HOWTO"
json.merge! hash
end

# => "post": { "title": "Merge HOWTO", "author": { "name": "David" } }

Array

Top level arrays can be handled directly. Useful for index and other collection actions.

1
2
3
4
5
6
7
8
9
10
11
12
13
# @comments = @post.comments

json.array! @comments do |comment|
next if comment.marked_as_spam_by?(current_user)

json.body comment.body
json.author do
json.first_name comment.author.first_name
json.last_name comment.author.last_name
end
end

# => [ { "body": "great post...", "author": { "first_name": "Joe", "last_name": "Bloe" }} ]

You can also extract attributes from array directly.

1
2
3
4
5
# @people = People.all

json.array! @people, :id, :name

# => [ { "id": 1, "name": "David" }, { "id": 2, "name": "Jamie" } ]

Jbuilder Object

Jbuilder objects can be directly nested inside each other. Useful for composing objects.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# app/jbuilder/person_jbuilder.rb

class Person
# ... Class Definition ... #
def to_builder
Jbuilder.new do |person|
person.(self, :name, :age)
end
end
end

class Company
# ... Class Definition ... #
def to_builder
Jbuilder.new do |company|
company.name name
company.president president.to_builder
end
end
end

company = Company.new('Doodle Corp', Person.new('John Stobs', 58))
company.to_builder.target!

# => {"name":"Doodle Corp","president":{"name":"John Stobs","age":58}}

You can either use Jbuilder stand-alone or directly as an ActionView template language. When required in Rails, you can create views a la show.json.jbuilder (the json is already yielded):

1
2
3
4
5
6
7
8
9
10
11
12
13
# Any helpers available to views are available to the builder
json.content format_content(@message.content)
json.(@message, :created_at, :updated_at)

json.author do
json.name @message.creator.name.familiar
json.email_address @message.creator.email_address_with_name
json.url url_for(@message.creator, format: :json)
end

if current_user.admin?
json.visitors calculate_visitors(@message)
end

Partial Render

You can use partials as well. The following will render the file views/comments/_comments.json.jbuilder, and set a local variable comments with all this message’s comments, which you can use inside the partial.

1
json.partial! 'comments/comments', comments: @message.comments

It’s also possible to render collections of partials:

1
2
3
4
5
6
7
8
9
10
11
12
13
json.array! @posts, partial: 'posts/post', as: :post

# or

json.partial! 'posts/post', collection: @posts, as: :post

# or

json.partial! partial: 'posts/post', collection: @posts, as: :post

# or

json.comments @post.comments, partial: 'comments/comment', as: :comment

The as: :some_symbol is used with partials. It will take care of mapping the passed in object to a variable for the partial. If the value is a collection (either implicitly or explicitly by using the collection: option, then each value of the collection is passed to the partial as the variable some_symbol. If the value is a singular object, then the object is passed to the partial as the variable some_symbol.

Be sure not to confuse the as: option to mean nesting of the partial. For example:

1
2
3
4
5
6
7
8
9
 # Use the default `views/comments/_comment.json.jbuilder`, putting @comment as the comment local variable.
# Note, `comment` attributes are "inlined".
json.partial! @comment, as: :comment
is quite different than:

# comment attributes are nested under a "comment" property
json.comment do
json.partial! "/comments/comment.json.jbuilder", comment: @comment
end

You can pass any objects into partial templates with or without :locals option.

1
2
3
4
5
json.partial! 'sub_template', locals: { user: user }

# or

json.partial! 'sub_template', user: user

You can explicitly make Jbuilder object return null if you want:

1
2
3
4
5
6
7
8
9
json.extract! @post, :id, :title, :content, :published_at
json.author do
if @post.anonymous?
json.null! # or json.nil!
else
json.first_name @post.author_first_name
json.last_name @post.author_last_name
end
end

To prevent Jbuilder from including null values in the output, you can use the ignore_nil! method:

1
2
3
4
json.ignore_nil!
json.foo nil
json.bar "bar"
# => { "bar": "bar" }

Fragment caching

Fragment caching is supported within rails/jbuilder: Jbuilder: generate JSON objects with a Builder-style DSL - https://github.com/rails/jbuilder, it uses Rails.cache and works like caching in HTML templates:

1
2
3
json.cache! ['v1', @person], expires_in: 10.minutes do
json.extract! @person, :name, :age
end

You can also conditionally cache a block by using cache_if! like this:

1
2
3
json.cache_if! !admin?, ['v1', @person], expires_in: 10.minutes do
json.extract! @person, :name, :age
end

If you are rendering fragments for a collection of objects, have a look at yonahforst/jbuilder_cache_multi - https://github.com/yonahforst/jbuilder_cache_multi gem. It uses fetch_multi (>= Rails 4.1) to fetch multiple keys at once.

Renders the given block for each item in the collection. Accepts optional ‘key’ attribute in options (e.g. key: ‘v1’).

Note: At the moment, does not accept the partial name as an argument (#todo)

Examples:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
json.cache_collection! @people, expires_in: 10.minutes do |person|
json.partial! 'person', :person => person
end

# Or with optional key

json.cache_collection! @people, expires_in: 10.minutes, key: 'v1' do |person|
json.partial! 'person', :person => person
end

# Or with a proc as a key

json.cache_collection! @people, expires_in: 10.minutes, key: proc {|person| person.last_posted_at } do |person|
json.partial! 'person', :person => person
end

Last thing: If you are using a collection for the cache key, may I recommend the ‘scope_cache_key’ gem? (check out my fork for a Rails 4 version: https://github.com/joshblour/scope_cache_key). It very quickly calculates a hash for all items in the collection (MD5 hash of updated_at + IDs).

You can also conditionally cache a block by using cache_collection_if! like this:

1
2
3
json.cache_collection_if! do_cache?, @people, expires_in: 10.minutes do |person|
json.partial! 'person', :person => person
end

Key Format

Keys can be auto formatted using key_format!. Specifies formatting to be applied to the key. Passing in a name of a function will cause that function to be called on the key. So :upcase will upper case the key. You can also pass in lambdas for more complex transformations.

Example:

1
2
3
4
5
6
7
json.key_format! :upcase
json.author do
json.name "David"
json.age 32
end

# => { "AUTHOR": { "NAME": "David", "AGE": 32 } }

You can pass parameters to the method using a hash pair.

1
2
3
4
json.key_format! camelize: :lower
json.first_name "David"

# => { "firstName": "David" }

Lambdas can also be used.

1
2
3
4
json.key_format! ->(key){ "_" + key }
json.first_name "David"

# => { "_first_name": "David" }

You can set this globally with the class method key_format (from inside your config/initializers/jbuilder.rb or environment.rb for example):

1
2
3
# config/initializers/jbuilder.rb

Jbuilder.key_format camelize: :lower

See lib/jbuilder.rb to learn more usages.

FAQS

return within block will cause json can not render content within block

1
2
3
4
5
6
7
8
9
10
11
12
13
return if data.blank?

json.data do
if data.is_a?(Enumerable)
json.rows data

# return within block will cause json can not render content within block.
# return unless data.respond_to?(:total_count)
if data.respond_to?(:total_count)
json.total data.total_count
end
end
end

References

[1] rails/jbuilder: Jbuilder: generate JSON objects with a Builder-style DSL - https://github.com/rails/jbuilder

[2] jbuilder/jbuilder.rb at 07d31ca6b8791b1767f9186a372ae3d3cbf16cf5 · rails/jbuilder - https://github.com/rails/jbuilder/blob/07d31ca6b8791b1767f9186a372ae3d3cbf16cf5/lib/jbuilder.rb

[3] yonahforst/jbuilder_cache_multi - https://github.com/yonahforst/jbuilder_cache_multi

[4] yonahforst/scope_cache_key: Add cache_key functionality to ActiveRecord scopes - https://github.com/yonahforst/scope_cache_key - https://github.com/yonahforst/scope_cache_key